#include "sdcardesp32cam.h"
#include "logging.h"
#include "scopedsemaphore.h"
#include <FS.h>
#include <SD_MMC.h>
#include <algorithm>
#include <esp_heap_caps.h>

namespace
{
constexpr char PHOTO_PREFIX[] = "photo_";
constexpr char PHOTO_EXTENSION[] = ".jpg";
constexpr char TEMP_EXTENSION[] = ".tmp";
constexpr TickType_t STORAGE_LOCK_TIMEOUT = pdMS_TO_TICKS(5000);

bool parseCanonicalPhotoNumber(
    const char *filename,
    const char *extension,
    uint32_t &photoNumber)
{
    if (filename == nullptr || extension == nullptr)
    {
        return false;
    }

    if (*filename == '/')
    {
        ++filename;
    }

    constexpr size_t PREFIX_LENGTH = sizeof(PHOTO_PREFIX) - 1;
    if (strncmp(filename, PHOTO_PREFIX, PREFIX_LENGTH) != 0)
    {
        return false;
    }

    const char *cursor = filename + PREFIX_LENGTH;

    // Numery zaczynają się od 1. Odrzucamy również zera wiodące.
    if (*cursor < '1' || *cursor > '9')
    {
        return false;
    }

    uint32_t parsedNumber = 0;
    while (*cursor >= '0' && *cursor <= '9')
    {
        const uint32_t digit = static_cast<uint32_t>(*cursor - '0');
        if (parsedNumber > (UINT32_MAX - digit) / 10U)
        {
            return false;
        }

        parsedNumber = parsedNumber * 10U + digit;
        ++cursor;
    }

    if (strcmp(cursor, extension) != 0)
    {
        return false;
    }

    photoNumber = parsedNumber;
    return true;
}
}

StorageSD::PhotoBuffer::~PhotoBuffer()
{
    reset();
}

StorageSD::PhotoBuffer::PhotoBuffer(PhotoBuffer &&other) noexcept
    : buffer(other.buffer), length(other.length)
{
    other.buffer = nullptr;
    other.length = 0;
}

StorageSD::PhotoBuffer &StorageSD::PhotoBuffer::operator=(
    PhotoBuffer &&other) noexcept
{
    if (this != &other)
    {
        reset();
        buffer = other.buffer;
        length = other.length;
        other.buffer = nullptr;
        other.length = 0;
    }
    return *this;
}

void StorageSD::PhotoBuffer::reset()
{
    if (buffer != nullptr)
    {
        heap_caps_free(buffer);
        buffer = nullptr;
    }
    length = 0;
}

bool StorageSD::begin()
{
    if (storageMutex == nullptr)
    {
        storageMutex = xSemaphoreCreateMutex();
    }

    if (storageMutex == nullptr)
    {
        LOG_PRINTLN("Failed to create SD card mutex");
        return false;
    }

    photoNumberReady = false;
    SD_MMC.end();

    if (!SD_MMC.begin("/sdcard", true))
    {
        LOG_PRINTLN("SD card init failed");
        return false;
    }

    const ScopedSemaphoreLock storageLock(
        storageMutex, STORAGE_LOCK_TIMEOUT);
    if (!storageLock)
    {
        LOG_PRINTLN("Failed to lock SD card during initialization");
        return false;
    }

    const bool tempFilesRemoved = removeOrphanedTempFilesUnlocked();
    const bool numberInitialized =
        tempFilesRemoved && initializeNextPhotoNumberUnlocked();

    if (!numberInitialized)
    {
        return false;
    }

    LOG_PRINTLN("SD card initialized");
    return true;
}

bool StorageSD::saveNextJpeg(
    camera_fb_t *frame,
    char *savedFilename,
    size_t savedFilenameSize)
{
    if (savedFilename == nullptr || savedFilenameSize == 0)
    {
        LOG_PRINTLN("Invalid output filename buffer");
        return false;
    }

    savedFilename[0] = '\0';

    const ScopedSemaphoreLock storageLock(
        storageMutex, STORAGE_LOCK_TIMEOUT);
    if (!storageLock)
    {
        LOG_PRINTLN("Failed to lock SD card");
        return false;
    }

    if (!photoNumberReady)
    {
        LOG_PRINTLN("Photo numbering is not available");
        return false;
    }

    uint32_t candidateNumber = nextPhotoNumber;
    char temporaryFilename[32];
    while (true)
    {
        const int filenameLength = snprintf(
            savedFilename,
            savedFilenameSize,
            "/photo_%lu.jpg",
            static_cast<unsigned long>(candidateNumber));

        if (filenameLength < 0 ||
            static_cast<size_t>(filenameLength) >= savedFilenameSize)
        {
            LOG_PRINTLN("Photo filename buffer is too small");
            savedFilename[0] = '\0';
            return false;
        }

        if (!SD_MMC.exists(savedFilename))
        {
            break;
        }

        LOG_PRINTF("Photo name collision: %s\n", savedFilename);

        if (candidateNumber == UINT32_MAX)
        {
            LOG_PRINTLN("Photo number range exhausted");
            photoNumberReady = false;
            savedFilename[0] = '\0';
            return false;
        }

        ++candidateNumber;
    }

    const int temporaryFilenameLength = snprintf(
        temporaryFilename,
        sizeof(temporaryFilename),
        "/photo_%lu.tmp",
        static_cast<unsigned long>(candidateNumber));

    if (temporaryFilenameLength < 0 ||
        static_cast<size_t>(temporaryFilenameLength) >= sizeof(temporaryFilename))
    {
        LOG_PRINTLN("Temporary photo filename is too long");
        savedFilename[0] = '\0';
        return false;
    }

    const bool result =
        saveJpegUnlocked(frame, temporaryFilename, savedFilename);
    if (result)
    {
        if (candidateNumber == UINT32_MAX)
        {
            photoNumberReady = false;
            LOG_PRINTLN("Photo number range exhausted after this save");
        }
        else
        {
            nextPhotoNumber = candidateNumber + 1U;
        }
    }
    else
    {
        savedFilename[0] = '\0';
    }

    return result;
}

StorageSD::Result StorageSD::listPhotoNumbers(
    std::vector<uint32_t> &photoNumbers,
    size_t maximumCount)
{
    photoNumbers.clear();

    const ScopedSemaphoreLock storageLock(
        storageMutex, STORAGE_LOCK_TIMEOUT);
    if (!storageLock)
    {
        return Result::Busy;
    }

    File root = SD_MMC.open("/");
    if (!root || !root.isDirectory())
    {
        if (root)
        {
            root.close();
        }
        return Result::Unavailable;
    }

    if (maximumCount > 0)
    {
        photoNumbers.reserve(maximumCount);
    }

    File file = root.openNextFile();
    while (file)
    {
        if (!file.isDirectory() && maximumCount > 0)
        {
            uint32_t photoNumber = 0;
            if (parsePhotoPath(file.name(), photoNumber))
            {
                auto position = std::lower_bound(
                    photoNumbers.begin(), photoNumbers.end(), photoNumber);

                if (position == photoNumbers.end() || *position != photoNumber)
                {
                    if (photoNumbers.size() < maximumCount)
                    {
                        photoNumbers.insert(position, photoNumber);
                    }
                    else if (photoNumber > photoNumbers.front())
                    {
                        photoNumbers.erase(photoNumbers.begin());
                        position = std::lower_bound(
                            photoNumbers.begin(), photoNumbers.end(), photoNumber);
                        photoNumbers.insert(position, photoNumber);
                    }
                }
            }
        }

        file.close();
        file = root.openNextFile();
    }

    root.close();
    return Result::Ok;
}

StorageSD::Result StorageSD::loadPhoto(
    const char *path,
    PhotoBuffer &photo,
    size_t maximumSizeBytes)
{
    photo.reset();

    uint32_t photoNumber = 0;
    if (!parsePhotoPath(path, photoNumber))
    {
        return Result::InvalidPath;
    }

    const ScopedSemaphoreLock storageLock(
        storageMutex, STORAGE_LOCK_TIMEOUT);
    if (!storageLock)
    {
        return Result::Busy;
    }

    File file = SD_MMC.open(path, FILE_READ);
    if (!file || file.isDirectory())
    {
        if (file)
        {
            file.close();
        }
        return Result::NotFound;
    }

    const size_t fileSize = file.size();
    if (fileSize < 4 || fileSize > maximumSizeBytes)
    {
        file.close();
        return Result::TooLarge;
    }

    const uint32_t memoryCapabilities = psramFound()
                                            ? MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
                                            : MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
    photo.buffer = static_cast<uint8_t *>(
        heap_caps_malloc(fileSize, memoryCapabilities));
    if (photo.buffer == nullptr)
    {
        file.close();
        return Result::OutOfMemory;
    }

    const size_t bytesRead = file.read(photo.buffer, fileSize);
    file.close();

    if (bytesRead != fileSize ||
        photo.buffer[0] != 0xFF || photo.buffer[1] != 0xD8 ||
        photo.buffer[fileSize - 2] != 0xFF ||
        photo.buffer[fileSize - 1] != 0xD9)
    {
        photo.reset();
        return Result::ReadError;
    }

    photo.length = fileSize;
    return Result::Ok;
}

bool StorageSD::parsePhotoPath(const char *path, uint32_t &photoNumber)
{
    return parseCanonicalPhotoNumber(path, PHOTO_EXTENSION, photoNumber);
}

bool StorageSD::removeOrphanedTempFilesUnlocked()
{
    uint32_t removedCount = 0;

    // Usuwamy po jednym pliku na skan, aby nie modyfikować katalogu podczas
    // działania jego iteratora i nie przechowywać nieograniczonej listy w RAM.
    while (true)
    {
        File root = SD_MMC.open("/");
        if (!root || !root.isDirectory())
        {
            LOG_PRINTLN("Failed to open root directory for temporary file cleanup");
            if (root)
            {
                root.close();
            }
            return false;
        }

        String orphanPath;
        File file = root.openNextFile();
        while (file)
        {
            if (!file.isDirectory())
            {
                uint32_t photoNumber = 0;
                if (parseCanonicalPhotoNumber(
                        file.name(), TEMP_EXTENSION, photoNumber))
                {
                    orphanPath = file.name();
                    if (!orphanPath.startsWith("/"))
                    {
                        orphanPath = "/" + orphanPath;
                    }
                    file.close();
                    break;
                }
            }

            file.close();
            file = root.openNextFile();
        }

        root.close();

        if (orphanPath.isEmpty())
        {
            LOG_PRINTF("Temporary photo cleanup complete; removed: %lu\n",
                       static_cast<unsigned long>(removedCount));
            return true;
        }

        if (!SD_MMC.remove(orphanPath.c_str()))
        {
            LOG_PRINTF("Failed to remove orphaned temporary file: %s\n",
                       orphanPath.c_str());
            return false;
        }

        ++removedCount;
    }
}

bool StorageSD::initializeNextPhotoNumberUnlocked()
{
    photoNumberReady = false;

    File root = SD_MMC.open("/");
    if (!root || !root.isDirectory())
    {
        LOG_PRINTLN("Failed to open root directory for photo scan");
        if (root)
        {
            root.close();
        }
        return false;
    }

    uint32_t highestNumber = 0;
    File file = root.openNextFile();

    while (file)
    {
        if (!file.isDirectory())
        {
            uint32_t photoNumber = 0;
            if (parseCanonicalPhotoNumber(
                    file.name(), PHOTO_EXTENSION, photoNumber) &&
                photoNumber > highestNumber)
            {
                highestNumber = photoNumber;
            }
        }

        file.close();
        file = root.openNextFile();
    }

    root.close();

    if (highestNumber == UINT32_MAX)
    {
        LOG_PRINTLN("Photo number range exhausted");
        return false;
    }

    nextPhotoNumber = highestNumber + 1U;
    photoNumberReady = true;

    LOG_PRINTF(
        "Photo numbering initialized; next number: %lu\n",
        static_cast<unsigned long>(nextPhotoNumber));
    return true;
}

bool StorageSD::saveJpegUnlocked(
    camera_fb_t *frame,
    const char *temporaryFilename,
    const char *finalFilename)
{
    if (frame == nullptr || frame->buf == nullptr || frame->len < 4)
    {
        LOG_PRINTLN("Invalid frame");
        return false;
    }

    if (frame->buf[0] != 0xFF || frame->buf[1] != 0xD8 ||
        frame->buf[frame->len - 2] != 0xFF ||
        frame->buf[frame->len - 1] != 0xD9)
    {
        LOG_PRINTLN("Camera frame does not contain complete JPEG markers");
        return false;
    }

    if (SD_MMC.exists(finalFilename))
    {
        LOG_PRINTF("Refusing to overwrite existing photo: %s\n", finalFilename);
        return false;
    }

    if (SD_MMC.exists(temporaryFilename) &&
        !SD_MMC.remove(temporaryFilename))
    {
        LOG_PRINTF("Failed to remove stale temporary file: %s\n",
                   temporaryFilename);
        return false;
    }

    // 8 kB = 16 sektorów po 512 bajtów.
    // Bezpieczny rozmiar dla wewnętrznej pamięci ESP32.
    constexpr size_t DMA_BUFFER_SIZE = 8 * 1024;

    uint8_t *dmaBuffer = static_cast<uint8_t *>(
        heap_caps_malloc(
            DMA_BUFFER_SIZE,
            MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA));

    if (dmaBuffer == nullptr)
    {
        LOG_PRINTLN("Failed to allocate DMA buffer");
        return false;
    }

    File file = SD_MMC.open(temporaryFilename, FILE_WRITE);

    if (!file)
    {
        LOG_PRINTLN("Failed to open temporary photo file");
        heap_caps_free(dmaBuffer);
        return false;
    }

#if VERBOSE_LOGGING
    const uint32_t startTime = millis();
#endif

    size_t totalWritten = 0;
    bool writeSuccess = true;

    while (totalWritten < frame->len)
    {
        const size_t remaining = frame->len - totalWritten;

        const size_t chunkSize =
            remaining > DMA_BUFFER_SIZE
                ? DMA_BUFFER_SIZE
                : remaining;

        // Kopiowanie z PSRAM do wewnętrznego RAM dostępnego dla DMA.
        memcpy(
            dmaBuffer,
            frame->buf + totalWritten,
            chunkSize);

        const size_t written = file.write(
            dmaBuffer,
            chunkSize);

        if (written != chunkSize)
        {
            LOG_PRINTF(
                "Write error: expected %u bytes, written %u bytes\n",
                static_cast<unsigned>(chunkSize),
                static_cast<unsigned>(written));

            writeSuccess = false;
            break;
        }

        totalWritten += written;
    }

    file.flush();
    file.close();

    heap_caps_free(dmaBuffer);

    if (!writeSuccess || totalWritten != frame->len)
    {
        // Usuwamy niepełny/uszkodzony plik.
        if (!SD_MMC.remove(temporaryFilename))
        {
            LOG_PRINTF("Failed to remove incomplete temporary file: %s\n",
                       temporaryFilename);
        }

        LOG_PRINTLN("Failed to save complete JPEG");
        return false;
    }

    if (!validateJpegFileUnlocked(temporaryFilename, frame->len))
    {
        if (!SD_MMC.remove(temporaryFilename))
        {
            LOG_PRINTF("Failed to remove invalid temporary file: %s\n",
                       temporaryFilename);
        }
        return false;
    }

    // Sprawdzamy ponownie bezpośrednio przed rename, ponieważ rename na FAT
    // może zastąpić plik docelowy.
    if (SD_MMC.exists(finalFilename))
    {
        LOG_PRINTF("Final photo appeared before rename: %s\n", finalFilename);
        if (!SD_MMC.remove(temporaryFilename))
        {
            LOG_PRINTF("Temporary file left for startup cleanup: %s\n",
                       temporaryFilename);
        }
        return false;
    }

    if (!SD_MMC.rename(temporaryFilename, finalFilename))
    {
        LOG_PRINTF("Failed to finalize photo: %s -> %s\n",
                   temporaryFilename,
                   finalFilename);
        if (!SD_MMC.remove(temporaryFilename))
        {
            LOG_PRINTF("Temporary file left for startup cleanup: %s\n",
                       temporaryFilename);
        }
        return false;
    }

    if (!SD_MMC.exists(finalFilename) || SD_MMC.exists(temporaryFilename))
    {
        LOG_PRINTF("Finalized photo verification failed: %s\n", finalFilename);
        return false;
    }

#if VERBOSE_LOGGING
    const uint32_t elapsedTime = millis() - startTime;
#endif

    LOG_PRINTF(
        "Saved: %s, %u bytes, %lu ms",
        finalFilename,
        static_cast<unsigned>(totalWritten),
        static_cast<unsigned long>(elapsedTime));

#if VERBOSE_LOGGING
    if (elapsedTime > 0)
    {
        const float speed =
            static_cast<float>(totalWritten) /
            static_cast<float>(elapsedTime);

        LOG_PRINTF(
            ", %.1f kB/s",
            speed);
    }
#endif

    LOG_PRINTLN();

    return true;
}

bool StorageSD::validateJpegFileUnlocked(
    const char *filename,
    size_t expectedSize)
{
    File file = SD_MMC.open(filename, FILE_READ);
    if (!file || file.isDirectory() || file.size() != expectedSize)
    {
        LOG_PRINTF("Temporary JPEG size validation failed: %s\n", filename);
        if (file)
        {
            file.close();
        }
        return false;
    }

    uint8_t marker[2];
    const bool hasStartMarker =
        file.read(marker, sizeof(marker)) == sizeof(marker) &&
        marker[0] == 0xFF && marker[1] == 0xD8;

    const bool seekSucceeded =
        expectedSize >= sizeof(marker) &&
        file.seek(expectedSize - sizeof(marker), SeekSet);
    const bool hasEndMarker =
        seekSucceeded &&
        file.read(marker, sizeof(marker)) == sizeof(marker) &&
        marker[0] == 0xFF && marker[1] == 0xD9;

    file.close();

    if (!hasStartMarker || !hasEndMarker)
    {
        LOG_PRINTF("Temporary JPEG marker validation failed: %s\n", filename);
        return false;
    }

    return true;
}
