#include "webgallery.h"
#include "appconfig.h"
#include "logging.h"
#include "scopedsemaphore.h"

#include <ESPmDNS.h>
#include <WiFi.h>
#include <algorithm>

#if __has_include("wifi_credentials.h")
#include "wifi_credentials.h"
#else
#include "wifi_credentials.example.h"
#endif

extern const uint8_t LOGO_PNG_START[]
    asm("_binary_logo_white_1000px_png_start");
extern const uint8_t LOGO_PNG_END[]
    asm("_binary_logo_white_1000px_png_end");

namespace
{
constexpr char HOSTNAME[] = "esp32-camera";
constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 20000;
constexpr uint32_t WIFI_RETRY_DELAY_MS = 10000;
constexpr TickType_t CACHE_LOCK_TIMEOUT = pdMS_TO_TICKS(5000);
constexpr size_t JSON_CHUNK_SIZE = 1024;

const char GALLERY_HTML[] PROGMEM = R"HTML(
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>ESP32 Camera</title>
  <style>
    body{margin:0;background:#111827;color:#f9fafb;font-family:Arial,sans-serif}
    header{padding:24px;text-align:center}h1{margin:0 0 8px}.hint{color:#9ca3af}
    .viewer{max-width:1000px;margin:auto;padding:0 16px 24px}
    .controls,.frame{background:#1f2937;border-radius:12px;padding:16px;margin-bottom:16px;box-shadow:0 4px 12px #0006}
    label{display:block;margin-bottom:8px;color:#d1d5db}
    select{width:100%;padding:11px;border-radius:8px;border:1px solid #4b5563;background:#111827;color:#fff}
    .actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
    button,.download{border:0;border-radius:8px;padding:11px 16px;background:#2563eb;color:#fff;text-decoration:none;font-size:14px;cursor:pointer}
    .download{background:#059669}.disabled{opacity:.45;pointer-events:none}
    .status{color:#93c5fd;margin-top:12px;min-height:20px}
    .error{color:#fca5a5}.frame{display:none}.frame.visible{display:block}
    .frame img{display:block;max-width:100%;max-height:72vh;margin:auto;border-radius:8px;background:#374151}
    .filename{text-align:center;color:#d1d5db;margin-top:12px;overflow-wrap:anywhere}
    footer{text-align:center;color:#9ca3af;padding:24px}
    .brand{max-width:520px;margin:0 auto 12px}
    .brand img{display:block;width:100%;height:auto}.credit{font-size:14px}
  </style>
</head>
<body>
  <header><h1>ESP32-CAM photo viewer</h1><div class="hint">Only the selected photo is transferred from the SD card</div></header>
  <main class="viewer">
    <section class="controls">
      <label for="photo-picker">Choose a photo</label>
      <select id="photo-picker" disabled><option>Loading photos...</option></select>
      <div class="actions">
        <button id="view-button" type="button" disabled>View selected</button>
        <a class="download disabled" id="download-link" download>Download selected</a>
      </div>
      <div class="status" id="status">Loading photo list...</div>
    </section>
    <section class="frame" id="photo-frame">
      <img id="photo-viewer" alt="Selected photo">
      <div class="filename" id="photo-name"></div>
    </section>
  </main>
  <footer>
    <div class="brand"><img src="/logo.png?v=3" alt="msalamon logo" loading="lazy"></div>
    <div class="credit">Desinged by msalamon</div>
  </footer>
  <script>
    const picker=document.getElementById('photo-picker');
    const viewButton=document.getElementById('view-button');
    const downloadLink=document.getElementById('download-link');
    const statusLabel=document.getElementById('status');
    const frame=document.getElementById('photo-frame');
    const image=document.getElementById('photo-viewer');
    const nameLabel=document.getElementById('photo-name');
    function photoUrl(path,download){return '/photo?name='+encodeURIComponent(path)+(download?'&download=1':'');}
    function updateSelection(){const path=picker.value;downloadLink.href=photoUrl(path,true);downloadLink.classList.remove('disabled');}
    function showPhoto(path){statusLabel.className='status';statusLabel.textContent='Loading '+path.substring(1)+'...';image.src=photoUrl(path,false);nameLabel.textContent=path.substring(1);frame.classList.add('visible');updateSelection();}
    image.addEventListener('load',()=>{statusLabel.textContent='Photo loaded.';});
    image.addEventListener('error',()=>{statusLabel.className='status error';statusLabel.textContent='Could not load the selected photo.';});
    picker.addEventListener('change',updateSelection);
    viewButton.addEventListener('click',()=>showPhoto(picker.value));
    async function loadPhotos(){
      try{
        const response=await fetch('/api/photos',{cache:'no-store'});
        if(!response.ok)throw new Error(await response.text());
        const photos=await response.json();
        picker.replaceChildren();
        if(photos.length===0){const option=document.createElement('option');option.textContent='No photos found';picker.appendChild(option);statusLabel.textContent='No photos found on the SD card.';return;}
        const options=document.createDocumentFragment();
        for(const path of photos){const option=document.createElement('option');option.value=path;option.textContent=path.substring(1);options.appendChild(option);}
        picker.appendChild(options);
        picker.disabled=false;viewButton.disabled=false;
        const latest=photos[photos.length-1];picker.value=latest;
        statusLabel.textContent=photos.length+' photo(s) found.';
        showPhoto(latest);
      }catch(error){statusLabel.className='status error';statusLabel.textContent='Photo list error: '+error.message;picker.replaceChildren();const option=document.createElement('option');option.textContent='Unavailable';picker.appendChild(option);}
    }
    loadPhotos();
  </script>
</body>
</html>
)HTML";
}

WebGallery::WebGallery() : server(80)
{
}

void WebGallery::begin(StorageSD &storageReference)
{
    storage = &storageReference;
    if (photoCacheMutex == nullptr)
    {
        photoCacheMutex = xSemaphoreCreateMutex();
    }

    cachedPhotoNumbers.reserve(AppConfig::PHOTO_CACHE_LIMIT);

    server.on("/", HTTP_GET, [this]() { handleGallery(); });
    server.on("/logo.png", HTTP_GET, [this]() { handleLogo(); });
    server.on("/api/photos", HTTP_GET, [this]() { handlePhotoList(); });
    server.on("/photo", HTTP_GET, [this]() { handlePhoto(); });
    server.on("/favicon.ico", HTTP_GET, [this]() { server.send(204); });
    server.onNotFound([this]() { handleNotFound(); });
}

void WebGallery::run()
{
    if (WIFI_SSID[0] == '\0')
    {
        LOG_PRINTLN("Wi-Fi gallery disabled: fill in src/wifi_credentials.h");
        return;
    }

    WiFi.useStaticBuffers(true);
    WiFi.mode(WIFI_STA);
    WiFi.setHostname(HOSTNAME);

    while (true)
    {
        if (!connectToWifi())
        {
            vTaskDelay(pdMS_TO_TICKS(WIFI_RETRY_DELAY_MS));
            continue;
        }

        const bool mdnsStarted = MDNS.begin(HOSTNAME);
        if (mdnsStarted)
        {
            MDNS.addService("http", "tcp", 80);
        }

        server.begin();

        LOG_PRINTLN();
        LOG_PRINTLN("Photo gallery started");
        LOG_PRINTF("IP address: http://%s/\n", WiFi.localIP().toString().c_str());
        if (mdnsStarted)
        {
            LOG_PRINTF("mDNS address: http://%s.local/\n", HOSTNAME);
        }

        while (WiFi.status() == WL_CONNECTED)
        {
            server.handleClient();
            vTaskDelay(pdMS_TO_TICKS(2));
        }

        server.stop();
        if (mdnsStarted)
        {
            MDNS.end();
        }

        LOG_PRINTLN("Wi-Fi disconnected; retrying...");
    }
}

bool WebGallery::connectToWifi()
{
    LOG_PRINTF("Connecting to Wi-Fi: %s", WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    const uint32_t startedAt = millis();
    while (WiFi.status() != WL_CONNECTED &&
           millis() - startedAt < WIFI_CONNECT_TIMEOUT_MS)
    {
        LOG_PRINT('.');
        vTaskDelay(pdMS_TO_TICKS(500));
    }

    if (WiFi.status() != WL_CONNECTED)
    {
        LOG_PRINTLN(" connection failed");
        WiFi.disconnect();
        return false;
    }

    WiFi.setSleep(false);
    LOG_PRINTLN(" connected");
    return true;
}

void WebGallery::handleGallery()
{
    server.sendHeader("Cache-Control", "no-store");
    server.send_P(200, PSTR("text/html; charset=utf-8"), GALLERY_HTML);
}

void WebGallery::handleLogo()
{
    const size_t logoSize =
        static_cast<size_t>(LOGO_PNG_END - LOGO_PNG_START);

    server.sendHeader("Cache-Control", "public, max-age=604800");
    server.send_P(
        200,
        PSTR("image/png"),
        reinterpret_cast<PGM_P>(LOGO_PNG_START),
        logoSize);
}

void WebGallery::handlePhotoList()
{
    std::vector<uint32_t> photoNumbers;
    const PhotoListResult result = getPhotoNumbers(photoNumbers);

    if (result == PhotoListResult::StorageBusy)
    {
        server.send(503, "text/plain", "SD card is busy. Try again.");
        return;
    }
    if (result == PhotoListResult::StorageUnavailable)
    {
        server.send(500, "text/plain", "Cannot open the SD card.");
        return;
    }
    if (result == PhotoListResult::CacheUnavailable)
    {
        server.send(500, "text/plain", "Photo cache is unavailable.");
        return;
    }

    server.setContentLength(CONTENT_LENGTH_UNKNOWN);
    server.sendHeader("Cache-Control", "no-store");
    server.send(200, "application/json", "");

    String chunk;
    chunk.reserve(JSON_CHUNK_SIZE + 32);
    chunk += '[';

    for (size_t index = 0; index < photoNumbers.size(); ++index)
    {
        char entry[40];
        const int entryLength = snprintf(
            entry,
            sizeof(entry),
            "%s\"/photo_%lu.jpg\"",
            index == 0 ? "" : ",",
            static_cast<unsigned long>(photoNumbers[index]));

        if (entryLength <= 0)
        {
            continue;
        }

        if (chunk.length() + static_cast<size_t>(entryLength) > JSON_CHUNK_SIZE)
        {
            server.sendContent(chunk);
            chunk = "";
        }

        chunk.concat(entry, static_cast<unsigned int>(entryLength));
    }

    chunk += ']';
    server.sendContent(chunk);
    server.sendContent("");
}

WebGallery::PhotoListResult WebGallery::getPhotoNumbers(
    std::vector<uint32_t> &photoNumbers)
{
#if VERBOSE_LOGGING
    const uint32_t startedAt = millis();
#endif

    if (photoCacheMutex == nullptr)
    {
        return PhotoListResult::CacheUnavailable;
    }

    const ScopedSemaphoreLock cacheLock(
        photoCacheMutex, CACHE_LOCK_TIMEOUT);
    if (!cacheLock)
    {
        return PhotoListResult::CacheUnavailable;
    }

    if (photoCacheValid)
    {
        photoNumbers = cachedPhotoNumbers;
        LOG_PRINTF("Photo list: %u files, cache hit in %lu ms\n",
                      static_cast<unsigned>(photoNumbers.size()),
                      static_cast<unsigned long>(millis() - startedAt));
        return PhotoListResult::Ok;
    }

    if (storage == nullptr)
    {
        return PhotoListResult::StorageUnavailable;
    }

    std::vector<uint32_t> refreshedNumbers;
    const StorageSD::Result storageResult = storage->listPhotoNumbers(
        refreshedNumbers, AppConfig::PHOTO_CACHE_LIMIT);

    if (storageResult == StorageSD::Result::Busy)
    {
        return PhotoListResult::StorageBusy;
    }
    if (storageResult != StorageSD::Result::Ok)
    {
        return PhotoListResult::StorageUnavailable;
    }

    cachedPhotoNumbers.swap(refreshedNumbers);
    photoCacheValid = true;
    photoNumbers = cachedPhotoNumbers;

    LOG_PRINTF("Photo list: %u files, generated in %lu ms\n",
                  static_cast<unsigned>(photoNumbers.size()),
                  static_cast<unsigned long>(millis() - startedAt));
    return PhotoListResult::Ok;
}

void WebGallery::notifyPhotoAdded(const char *filename)
{
    if (filename == nullptr || photoCacheMutex == nullptr)
    {
        return;
    }

    uint32_t photoNumber = 0;
    if (!StorageSD::parsePhotoPath(filename, photoNumber))
    {
        return;
    }

    const ScopedSemaphoreLock cacheLock(
        photoCacheMutex, CACHE_LOCK_TIMEOUT);
    if (!cacheLock)
    {
        return;
    }

    if (photoCacheValid)
    {
        const auto position = std::lower_bound(
            cachedPhotoNumbers.begin(),
            cachedPhotoNumbers.end(),
            photoNumber);

        if (position == cachedPhotoNumbers.end() || *position != photoNumber)
        {
            if (cachedPhotoNumbers.size() >= AppConfig::PHOTO_CACHE_LIMIT)
            {
                if (photoNumber <= cachedPhotoNumbers.front())
                {
                    return;
                }
                cachedPhotoNumbers.erase(cachedPhotoNumbers.begin());
            }

            const auto limitedPosition = std::lower_bound(
                cachedPhotoNumbers.begin(),
                cachedPhotoNumbers.end(),
                photoNumber);
            cachedPhotoNumbers.insert(limitedPosition, photoNumber);
        }
    }
}

void WebGallery::handlePhoto()
{
    if (!server.hasArg("name"))
    {
        server.send(400, "text/plain", "Missing photo name.");
        return;
    }

    const String path = server.arg("name");
    uint32_t photoNumber = 0;
    if (!StorageSD::parsePhotoPath(path.c_str(), photoNumber))
    {
        server.send(400, "text/plain", "Invalid photo name.");
        return;
    }

    if (storage == nullptr)
    {
        server.send(500, "text/plain", "SD storage is unavailable.");
        return;
    }

    StorageSD::PhotoBuffer photo;
#if VERBOSE_LOGGING
    const uint32_t loadStartedAt = millis();
#endif
    const StorageSD::Result loadResult = storage->loadPhoto(
        path.c_str(), photo, AppConfig::WEB_PHOTO_BUFFER_LIMIT_BYTES);
#if VERBOSE_LOGGING
    const uint32_t loadElapsed = millis() - loadStartedAt;
#endif

    if (loadResult != StorageSD::Result::Ok)
    {
        switch (loadResult)
        {
        case StorageSD::Result::Busy:
            server.send(503, "text/plain", "SD card is busy. Try again.");
            break;
        case StorageSD::Result::InvalidPath:
            server.send(400, "text/plain", "Invalid photo name.");
            break;
        case StorageSD::Result::NotFound:
            server.send(404, "text/plain", "Photo not found.");
            break;
        case StorageSD::Result::TooLarge:
            server.send(413, "text/plain", "Photo is too large to transfer.");
            break;
        case StorageSD::Result::OutOfMemory:
            server.send(503, "text/plain", "Not enough memory to transfer photo.");
            break;
        default:
            server.send(500, "text/plain", "Cannot read photo from SD card.");
            break;
        }
        return;
    }

    server.sendHeader("Cache-Control", "public, max-age=3600");
    if (server.hasArg("download"))
    {
        String disposition = F("attachment; filename=\"");
        disposition += path.substring(1);
        disposition += '"';
        server.sendHeader("Content-Disposition", disposition);
    }
    const size_t photoSize = photo.size();
    server.setContentLength(photoSize);
    server.send(200, "image/jpeg", "");

#if VERBOSE_LOGGING
    const uint32_t transferStartedAt = millis();
#endif
    WiFiClient client = server.client();
    const size_t transferred = client.write(photo.data(), photoSize);
#if VERBOSE_LOGGING
    const uint32_t transferElapsed = millis() - transferStartedAt;
#endif

    if (transferred != photoSize)
    {
        LOG_PRINTF("Photo transfer interrupted: expected %u, sent %u bytes\n",
                   static_cast<unsigned>(photoSize),
                   static_cast<unsigned>(transferred));
        client.stop();
        return;
    }

    LOG_PRINTF(
        "Photo transfer: loaded %u bytes from SD in %lu ms; sent %u in %lu ms\n",
        static_cast<unsigned>(photoSize),
        static_cast<unsigned long>(loadElapsed),
        static_cast<unsigned>(transferred),
        static_cast<unsigned long>(transferElapsed));
    LOG_PRINTF(
        "Web task minimum free stack: %u bytes\n",
        static_cast<unsigned>(uxTaskGetStackHighWaterMark(nullptr)));
}

void WebGallery::handleNotFound()
{
    server.send(404, "text/plain", "Not found. Open / to view the gallery.");
}
