#include "http_dashboard.h"

#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>

#include "app_state.h"
#include "esp_camera.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "portrait_engine.h"

static const char *TAG = "http_dashboard";
static httpd_handle_t s_server = NULL;

static const char INDEX_HTML[] =
    "<!doctype html><html><head><meta charset=\"utf-8\">"
    "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
    "<title>SCARA Portrait</title>"
    "<style>"
    ":root{font-family:system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:#16201c;background:#f5f6f2}"
    "body{margin:0}.top{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:#ffffff;border-bottom:1px solid #d8ddd5}"
    "h1{font-size:18px;margin:0;font-weight:650;letter-spacing:0}.status{font-size:13px;color:#51605a}"
    "main{display:grid;grid-template-columns:1fr 1fr;gap:14px;padding:14px;max-width:1180px;margin:0 auto}"
    "section{background:#fff;border:1px solid #d8ddd5;border-radius:8px;overflow:hidden;min-width:0}"
    "section header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #e8ebe6;font-size:13px;color:#51605a}"
    "img,canvas{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:contain;background:#eef1ee}"
    "canvas{background:#fff}.controls{display:flex;gap:8px;align-items:center}"
    "button{appearance:none;border:1px solid #1c6b54;background:#1c6b54;color:#fff;border-radius:6px;padding:8px 12px;font-weight:650;cursor:pointer}"
    "button.secondary{background:#fff;color:#1c6b54}.meta{padding:10px 12px;font-size:12px;color:#51605a;min-height:18px}"
    "@media(max-width:760px){main{grid-template-columns:1fr}.top{align-items:flex-start;gap:8px;flex-direction:column}}"
    "</style></head><body>"
    "<div class=\"top\"><h1>SCARA Portrait</h1><div class=\"controls\"><span class=\"status\" id=\"status\">booting</span><button id=\"capture\">Capture</button><button class=\"secondary\" id=\"refresh\">Refresh</button></div></div>"
    "<main>"
    "<section><header><span>Live camera</span><span id=\"face\">face: -</span></header><img id=\"live\" src=\"/frame\"><div class=\"meta\" id=\"overlay\">overlay: -</div></section>"
    "<section><header><span>Portrait path</span><span id=\"count\">0 strokes</span></header><canvas id=\"portrait\" width=\"320\" height=\"240\"></canvas><div class=\"meta\" id=\"message\">waiting</div></section>"
    "<section><header><span>Captured frame</span><span id=\"shotTime\">latest</span></header><img id=\"shot\" src=\"/shot\"><div class=\"meta\">JPEG captured from the button or browser action.</div></section>"
    "</main>"
    "<script>"
    "const $=id=>document.getElementById(id);let liveTimer=null;"
    "function startLive(){if(liveTimer)return;liveTimer=setInterval(()=>{$('live').src='/frame?t='+Date.now();},350);}"
    "function stopLive(){if(liveTimer){clearInterval(liveTimer);liveTimer=null;}}"
    "function draw(data){const c=$('portrait'),ctx=c.getContext('2d');const w=data.canvas_size?.[0]||320,h=data.canvas_size?.[1]||240;c.width=w;c.height=h;ctx.clearRect(0,0,w,h);ctx.fillStyle='#fff';ctx.fillRect(0,0,w,h);ctx.lineWidth=1.4;ctx.lineCap='round';ctx.lineJoin='round';for(const s of data.strokes||[]){ctx.beginPath();(s.points||[]).forEach((p,i)=>i?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]));ctx.strokeStyle=s.type==='xdog'?'#111':(s.type==='mouth'?'#0b6e4f':'#7a2f6f');ctx.stroke();}$('count').textContent=(data.strokes||[]).length+' strokes';$('message').textContent=data.message||'';}"
    "async function loadPortrait(){const r=await fetch('/portrait',{cache:'no-store'});draw(await r.json());}"
    "async function loadStatus(){const r=await fetch('/status',{cache:'no-store'}),s=await r.json();$('status').textContent=s.mode+' / '+s.state;}"
    "async function loadOverlay(){const r=await fetch('/overlay',{cache:'no-store'}),o=await r.json();$('face').textContent=o.face.detected?('face: '+Math.round(o.face.score*100)+'%'):'face: none';$('overlay').textContent=o.face.detected?('bbox ['+o.face.bbox.join(', ')+']'):'overlay: no face';}"
    "$('capture').onclick=async()=>{ stopLive();$('status').textContent='capturing'; await fetch('/capture',{cache:'no-store'}); $('shot').src='/shot?t='+Date.now(); await loadPortrait(); await loadStatus(); await loadOverlay(); startLive(); };"
    "$('refresh').onclick=async()=>{ $('shot').src='/shot?t='+Date.now(); await loadPortrait(); await loadStatus(); await loadOverlay(); };"
    "setInterval(()=>{loadStatus();loadOverlay();},2000);startLive();loadPortrait();loadStatus();loadOverlay();"
    "</script></body></html>";

static const char *mode_name(app_mode_t mode)
{
    switch (mode) {
    case MODE_STOPPED:
        return "STOPPED";
    case MODE_RUNNING:
        return "RUNNING";
    default:
        return "UNKNOWN";
    }
}

static const char *state_name(app_state_t state)
{
    switch (state) {
    case STATE_IDLE:
        return "IDLE";
    case STATE_DETECTING:
        return "DETECTING";
    case STATE_CAPTURING:
        return "CAPTURING";
    case STATE_DRAWING:
        return "DRAWING";
    case STATE_COOLDOWN:
        return "COOLDOWN";
    default:
        return "UNKNOWN";
    }
}

static esp_err_t send_chunkf(httpd_req_t *req, const char *fmt, ...)
{
    char buf[192];
    va_list args;
    va_start(args, fmt);
    int len = vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    if (len < 0) {
        return ESP_FAIL;
    }
    if (len >= (int)sizeof(buf)) {
        len = sizeof(buf) - 1;
    }
    return httpd_resp_send_chunk(req, buf, len);
}

static esp_err_t root_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html; charset=utf-8");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    return httpd_resp_send(req, INDEX_HTML, HTTPD_RESP_USE_STRLEN);
}

static esp_err_t status_handler(httpd_req_t *req)
{
    portrait_engine_lock();
    const portrait_result_t *result = portrait_engine_get_result_locked();
    const char *message = result->message;
    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    esp_err_t err = send_chunkf(req,
                                "{\"mode\":\"%s\",\"state\":\"%s\",\"message\":\"%s\",\"timestamp\":%u}",
                                mode_name(app_state_get_mode()),
                                state_name(app_state_get_state()),
                                message,
                                (unsigned)result->timestamp_ms);
    portrait_engine_unlock();
    if (err == ESP_OK) {
        err = httpd_resp_send_chunk(req, NULL, 0);
    }
    return err;
}

static esp_err_t overlay_handler(httpd_req_t *req)
{
    portrait_engine_lock();
    const portrait_result_t *result = portrait_engine_get_result_locked();
    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    esp_err_t err = ESP_OK;
    if (result->has_face) {
        err = send_chunkf(req,
                          "{\"state\":\"%s\",\"face\":{\"detected\":true,\"score\":%.3f,"
                          "\"bbox\":[%d,%d,%d,%d],\"left_eye\":[%d,%d],\"left_mouth\":[%d,%d],"
                          "\"nose\":[%d,%d],\"right_eye\":[%d,%d],\"right_mouth\":[%d,%d]}}",
                          state_name(app_state_get_state()),
                          result->face_score,
                          result->bbox[0],
                          result->bbox[1],
                          result->bbox[2],
                          result->bbox[3],
                          result->keypoint[0],
                          result->keypoint[1],
                          result->keypoint[2],
                          result->keypoint[3],
                          result->keypoint[4],
                          result->keypoint[5],
                          result->keypoint[6],
                          result->keypoint[7],
                          result->keypoint[8],
                          result->keypoint[9]);
    } else {
        err = send_chunkf(req, "{\"state\":\"%s\",\"face\":{\"detected\":false}}", state_name(app_state_get_state()));
    }
    portrait_engine_unlock();
    if (err == ESP_OK) {
        err = httpd_resp_send_chunk(req, NULL, 0);
    }
    return err;
}

static esp_err_t portrait_handler(httpd_req_t *req)
{
    portrait_engine_lock();
    const portrait_result_t *result = portrait_engine_get_result_locked();
    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");

    esp_err_t err = send_chunkf(req,
                                "{\"timestamp\":%u,\"message\":\"%s\",\"canvas_size\":[%d,%d],\"strokes\":[",
                                (unsigned)result->timestamp_ms,
                                result->message,
                                result->canvas_w,
                                result->canvas_h);

    for (uint16_t i = 0; err == ESP_OK && i < result->stroke_count; ++i) {
        const portrait_stroke_t *stroke = &result->strokes[i];
        if (i > 0) {
            err = httpd_resp_send_chunk(req, ",", 1);
        }
        if (err != ESP_OK) {
            break;
        }
        err = send_chunkf(req, "{\"type\":\"%s\",\"points\":[", portrait_stroke_type_name(stroke->type));
        for (uint16_t p = 0; err == ESP_OK && p < stroke->point_count; ++p) {
            const portrait_point_t *point = &result->points[stroke->first_point + p];
            if (p > 0) {
                err = httpd_resp_send_chunk(req, ",", 1);
            }
            if (err == ESP_OK) {
                err = send_chunkf(req, "[%d,%d]", point->x, point->y);
            }
        }
        if (err == ESP_OK) {
            err = httpd_resp_send_chunk(req, "]}", 2);
        }
    }

    if (err == ESP_OK) {
        err = httpd_resp_send_chunk(req, "]}", 2);
    }
    portrait_engine_unlock();
    if (err == ESP_OK) {
        err = httpd_resp_send_chunk(req, NULL, 0);
    }
    return err;
}

static esp_err_t shot_handler(httpd_req_t *req)
{
    portrait_engine_lock();
    size_t len = 0;
    const uint8_t *jpeg = portrait_engine_get_jpeg_locked(&len);
    if (jpeg == NULL || len == 0) {
        portrait_engine_unlock();
        return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "No shot captured yet");
    }

    httpd_resp_set_type(req, "image/jpeg");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    esp_err_t err = httpd_resp_send(req, (const char *)jpeg, len);
    portrait_engine_unlock();
    return err;
}

static esp_err_t frame_handler(httpd_req_t *req)
{
    camera_fb_t *fb = esp_camera_fb_get();
    if (fb == NULL) {
        ESP_LOGW(TAG, "frame capture failed");
        return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Frame capture failed");
    }

    if (fb->format != PIXFORMAT_JPEG) {
        esp_camera_fb_return(fb);
        return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Frame was not JPEG");
    }

    httpd_resp_set_type(req, "image/jpeg");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    esp_err_t err = httpd_resp_send(req, (const char *)fb->buf, fb->len);
    esp_camera_fb_return(fb);
    return err;
}

static esp_err_t capture_handler(httpd_req_t *req)
{
    ESP_LOGI(TAG, "browser requested capture");
    esp_err_t capture_err = portrait_engine_capture_and_wait(45000);
    ESP_LOGI(TAG, "browser capture finished: %s", esp_err_to_name(capture_err));
    char body[80];
    int len = snprintf(body,
                       sizeof(body),
                       "{\"ok\":%s,\"error\":\"%s\"}",
                       capture_err == ESP_OK ? "true" : "false",
                       esp_err_to_name(capture_err));
    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    return httpd_resp_send(req, body, len);
}

esp_err_t http_dashboard_start(void)
{
    if (s_server != NULL) {
        return ESP_OK;
    }

    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;
    config.stack_size = 8192;
    config.max_open_sockets = 7;

    esp_err_t err = httpd_start(&s_server, &config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "httpd_start failed: %s", esp_err_to_name(err));
        return err;
    }

    const httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_handler, .user_ctx = NULL};
    const httpd_uri_t status = {.uri = "/status", .method = HTTP_GET, .handler = status_handler, .user_ctx = NULL};
    const httpd_uri_t overlay = {.uri = "/overlay", .method = HTTP_GET, .handler = overlay_handler, .user_ctx = NULL};
    const httpd_uri_t portrait = {.uri = "/portrait", .method = HTTP_GET, .handler = portrait_handler, .user_ctx = NULL};
    const httpd_uri_t shot = {.uri = "/shot", .method = HTTP_GET, .handler = shot_handler, .user_ctx = NULL};
    const httpd_uri_t frame = {.uri = "/frame", .method = HTTP_GET, .handler = frame_handler, .user_ctx = NULL};
    const httpd_uri_t capture = {.uri = "/capture", .method = HTTP_GET, .handler = capture_handler, .user_ctx = NULL};

    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &root));
    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &status));
    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &overlay));
    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &portrait));
    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &shot));
    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &frame));
    ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &capture));

    ESP_LOGI(TAG, "dashboard started on port 80");
    return ESP_OK;
}
