#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <time.h>

#include "../../platform.h"

#if defined(_WIN32) && !defined(WIN32)
#define WIN32
#endif

#ifdef WIN32
#include <windows.h>
#include <io.h>
#include <process.h>
#include <sys/stat.h>
#define unlink _unlink
#define close _close
typedef intptr_t pid_t;
static int ows_mkstemp(char *templ)
{
    if (!templ || _mktemp_s(templ, strlen(templ) + 1) != 0)
        return -1;
    return _open(templ, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
}
#define mkstemp ows_mkstemp
#else
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif

#include "../modHeader.h"
#if defined(__has_include)
  #if __has_include(<mysql/mysql.h>)
    #include <mysql/mysql.h>
  #elif __has_include(<mysql.h>)
    #include <mysql.h>
  #elif __has_include(<mariadb/mysql.h>)
    #include <mariadb/mysql.h>
  #else
    #include "../../mysql/mysql.h"
  #endif
#else
  #include "../../mysql/mysql.h"
#endif
#include "../../macro.h"

#ifndef MAXPACKETBUFSIZE
#define MAXPACKETBUFSIZE 200000
#endif

#ifdef WIN32
#define OWS_VIDEO_TMP_IN_TEMPLATE ".\\ows_media_probe_XXXXXX"
#else
#define OWS_VIDEO_TMP_IN_TEMPLATE OWS_TMP_TEMPLATE("ows_media_probe_XXXXXX")
#endif

typedef struct MEDIA_META_TAG
{
    char mime[64];
    char title[256];
    char description[512];
    char artist[256];
    char album[256];
    char videoCodec[64];
    char audioCodec[64];
    char fps[32];
    char duration[64];
    char bitRate[64];
    char sampleRate[64];
    char channels[32];
    char mediaKind[32];
    unsigned int width;
    unsigned int height;
    unsigned int sizeBytes;
} MEDIA_META;

static char gFfprobePath[512];
static unsigned int gProbeEnabled = 1;
static unsigned int gProbeMaxBytes = 50000000;
static unsigned int gProbeTimeoutMs = 8000;
static unsigned int gProbeThreadsMax = 1;
static unsigned int gSkipStreaming = 1;
static int gDebugKeepTmp = 0;
static volatile int gProbeActive = 0;
static int gMediaSchemaReady = 0;

static int starts_with(const char *s, const char *prefix)
{
    if (!s || !prefix) return 0;
    return strncmp(s, prefix, strlen(prefix)) == 0;
}

static int ends_with_ci(const char *s, const char *suffix)
{
    size_t sl, xl, i;
    if (!s || !suffix) return 0;
    sl = strlen(s);
    xl = strlen(suffix);
    if (sl < xl) return 0;
    for (i = 0; i < xl; i++)
    {
        if (tolower((unsigned char)s[sl - xl + i]) != tolower((unsigned char)suffix[i]))
            return 0;
    }
    return 1;
}

static int str_contains_ci(const char *hay, const char *needle)
{
    size_t i, j, hn, nn;
    if (!hay || !needle) return 0;
    hn = strlen(hay);
    nn = strlen(needle);
    if (nn == 0 || hn < nn) return 0;
    for (i = 0; i <= hn - nn; i++)
    {
        for (j = 0; j < nn; j++)
        {
            if (tolower((unsigned char)hay[i + j]) != tolower((unsigned char)needle[j]))
                break;
        }
        if (j == nn)
            return 1;
    }
    return 0;
}

static int mem_contains_ci(const char *hay, size_t hayLen, const char *needle)
{
    size_t i, j, nn;
    if (!hay || !needle) return 0;
    nn = strlen(needle);
    if (nn == 0 || hayLen < nn) return 0;
    for (i = 0; i <= hayLen - nn; i++)
    {
        for (j = 0; j < nn; j++)
        {
            if (tolower((unsigned char)hay[i + j]) != tolower((unsigned char)needle[j]))
                break;
        }
        if (j == nn)
            return 1;
    }
    return 0;
}

static int page_has_suffix_ci(const char *page, const char *suffix)
{
    char pageOnly[MAXPAGESIZE];
    const char *qmark;
    const char *hash;
    size_t len;

    if (!page || !suffix) return 0;
    qmark = strchr(page, '?');
    hash = strchr(page, '#');
    if (qmark && hash)
        len = (qmark < hash) ? (size_t)(qmark - page) : (size_t)(hash - page);
    else if (qmark)
        len = (size_t)(qmark - page);
    else if (hash)
        len = (size_t)(hash - page);
    else
        len = strlen(page);
    if (len >= sizeof(pageOnly)) len = sizeof(pageOnly) - 1;
    memcpy(pageOnly, page, len);
    pageOnly[len] = '\0';
    return ends_with_ci(pageOnly, suffix);
}

static int brand_is(const unsigned char *buf, const char *brand)
{
    return buf && brand && memcmp(buf + 8, brand, 4) == 0;
}

static void trim_inplace(char *s)
{
    int start = 0, end, i;
    if (!s) return;
    while (s[start] && isspace((unsigned char)s[start])) start++;
    if (start > 0) memmove(s, s + start, strlen(s + start) + 1);
    end = (int)strlen(s) - 1;
    while (end >= 0 && isspace((unsigned char)s[end])) s[end--] = '\0';
    for (i = 0; s[i]; i++)
        if ((unsigned char)s[i] < 32 && s[i] != '\t' && s[i] != '\n')
            s[i] = ' ';
}

static int read_file_into_buffer(const char *path, char *out, size_t outSize)
{
    FILE *fp;
    size_t n;

    if (!path || !out || outSize < 2) return 0;
    out[0] = '\0';
    fp = fopen(path, "rb");
    if (!fp) return 0;
    n = fread(out, 1, outSize - 1, fp);
    out[n] = '\0';
    fclose(fp);
    return (int)n;
}

static void sanitize_token_copy(const char *src, char *dst, size_t dstSize)
{
    size_t i, j = 0;
    if (!src || !dst || dstSize < 2)
    {
        if (dst && dstSize) dst[0] = '\0';
        return;
    }

    for (i = 0; src[i] && j + 1 < dstSize; i++)
    {
        unsigned char c = (unsigned char)src[i];
        if (isalnum(c))
            dst[j++] = (char)tolower(c);
        else if (c == '_' || c == '-' || c == '.' || c == '/' || isspace(c))
            dst[j++] = ' ';
    }
    dst[j] = '\0';
    trim_inplace(dst);
}

static void append_field(char *dst, size_t dstSize, const char *label, const char *value)
{
    char clean[1024];
    size_t curLen, need;

    if (!dst || !label || !value || value[0] == '\0')
        return;
    sanitize_token_copy(value, clean, sizeof(clean));
    if (clean[0] == '\0')
        return;
    curLen = strlen(dst);
    need = strlen(label) + strlen(clean) + 3;
    if (curLen + need >= dstSize)
        return;
    if (curLen > 0) strcat(dst, " ");
    strcat(dst, label);
    strcat(dst, " ");
    strcat(dst, clean);
}

static void append_number_field(char *dst, size_t dstSize, const char *label, unsigned int value)
{
    char num[64];
    if (value == 0) return;
    snprintf(num, sizeof(num), "%u", value);
    append_field(dst, dstSize, label, num);
}

static void basename_from_path(const struct sHost *host, char *out, size_t outSize)
{
    const char *start, *end;
    char raw[512];
    size_t len;

    if (!host || !out || outSize < 2) return;
    out[0] = '\0';
    start = strrchr(host->Page, '/');
    start = start ? start + 1 : host->Page;
    end = start;
    while (*end && *end != '?' && *end != '#') end++;
    len = (size_t)(end - start);
    if (len == 0 || len >= sizeof(raw)) return;
    memcpy(raw, start, len);
    raw[len] = '\0';
    end = strrchr(raw, '.');
    if (end && end > raw)
        raw[end - raw] = '\0';
    sanitize_token_copy(raw, out, outSize);
}

static int is_stream_playlist(const struct sHost *host)
{
    if (!host) return 0;
    if (str_contains_ci(host->HttpContentType, "application/vnd.apple.mpegurl") ||
        str_contains_ci(host->HttpContentType, "application/x-mpegurl") ||
        str_contains_ci(host->HttpContentType, "application/dash+xml"))
        return 1;
    if (page_has_suffix_ci(host->Page, ".m3u8") || page_has_suffix_ci(host->Page, ".mpd"))
        return 1;
    return 0;
}

static int detect_media(const struct functArg *arg, MEDIA_META *meta)
{
    const char *ct;
    const unsigned char *buf;
    size_t len;
    if (!arg || !arg->hostInfo || !meta) return 0;
    ct = arg->hostInfo->HttpContentType;
    buf = (const unsigned char *)arg->html;
    len = arg->htmlLength;
    if (ct[0])
        strncpy(meta->mime, ct, sizeof(meta->mime) - 1);

    if (str_contains_ci(ct, "video/"))
    {
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (str_contains_ci(ct, "audio/"))
    {
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (str_contains_ci(ct, "application/vnd.apple.mpegurl") ||
        str_contains_ci(ct, "application/x-mpegurl") ||
        str_contains_ci(ct, "application/dash+xml"))
    {
        if (meta->mime[0] == '\0') strncpy(meta->mime, str_contains_ci(ct, "dash+xml") ? "application/dash+xml" : "application/vnd.apple.mpegurl", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "stream", sizeof(meta->mediaKind) - 1);
        return 1;
    }

    if (page_has_suffix_ci(arg->hostInfo->Page, ".mp4") || page_has_suffix_ci(arg->hostInfo->Page, ".webm") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".ogv") || page_has_suffix_ci(arg->hostInfo->Page, ".mov") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".avi") || page_has_suffix_ci(arg->hostInfo->Page, ".mkv") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".m4v") || page_has_suffix_ci(arg->hostInfo->Page, ".flv") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".wmv") || page_has_suffix_ci(arg->hostInfo->Page, ".asf") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".ts") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".m2ts") || page_has_suffix_ci(arg->hostInfo->Page, ".3gp") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".3g2") || page_has_suffix_ci(arg->hostInfo->Page, ".mpeg") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".mpg") || page_has_suffix_ci(arg->hostInfo->Page, ".mpe") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".vob"))
    {
        if (meta->mime[0] == '\0') strncpy(meta->mime, "video/unknown", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }

    if (page_has_suffix_ci(arg->hostInfo->Page, ".mp3") || page_has_suffix_ci(arg->hostInfo->Page, ".m4a") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".aac") || page_has_suffix_ci(arg->hostInfo->Page, ".oga") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".ogg") || page_has_suffix_ci(arg->hostInfo->Page, ".wav") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".flac") || page_has_suffix_ci(arg->hostInfo->Page, ".wma") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".opus") || page_has_suffix_ci(arg->hostInfo->Page, ".weba") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".m4b") || page_has_suffix_ci(arg->hostInfo->Page, ".adts") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".aif") || page_has_suffix_ci(arg->hostInfo->Page, ".aiff") ||
        page_has_suffix_ci(arg->hostInfo->Page, ".mid") || page_has_suffix_ci(arg->hostInfo->Page, ".midi"))
    {
        if (meta->mime[0] == '\0') strncpy(meta->mime, "audio/unknown", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }

    if (page_has_suffix_ci(arg->hostInfo->Page, ".m3u8") || page_has_suffix_ci(arg->hostInfo->Page, ".mpd"))
    {
        if (meta->mime[0] == '\0') strncpy(meta->mime, page_has_suffix_ci(arg->hostInfo->Page, ".mpd") ? "application/dash+xml" : "application/vnd.apple.mpegurl", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "stream", sizeof(meta->mediaKind) - 1);
        return 1;
    }

    if (buf && len >= 7 && memcmp(buf, "#EXTM3U", 7) == 0)
    {
        if (meta->mime[0] == '\0') strncpy(meta->mime, "application/vnd.apple.mpegurl", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "stream", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 32 && mem_contains_ci((const char*)buf, len, "<MPD"))
    {
        if (meta->mime[0] == '\0') strncpy(meta->mime, "application/dash+xml", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "stream", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 12 && memcmp(buf + 4, "ftyp", 4) == 0 &&
        (brand_is(buf, "M4A ") || brand_is(buf, "M4B ")))
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/mp4", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 12 && memcmp(buf + 4, "ftyp", 4) == 0 &&
        (brand_is(buf, "avif") || brand_is(buf, "avis") || brand_is(buf, "heic") ||
         brand_is(buf, "heix") || brand_is(buf, "hevc") || brand_is(buf, "hevx")))
    {
        return 0;
    }
    if (buf && len >= 12 && memcmp(buf + 4, "ftyp", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/mp4", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 4 && memcmp(buf, "\x1a\x45\xdf\xa3", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/webm", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 12 && memcmp(buf, "RIFF", 4) == 0 && memcmp(buf + 8, "AVI ", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/avi", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 4 && memcmp(buf, "FLV", 3) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/x-flv", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 16 && memcmp(buf, "\x30\x26\xb2\x75\x8e\x66\xcf\x11\xa6\xd9\x00\xaa\x00\x62\xce\x6c", 16) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/x-ms-asf", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 4 && memcmp(buf, "\x00\x00\x01\xba", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/mpeg", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 189 && buf[0] == 0x47 && buf[188] == 0x47)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "video/mp2t", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "video", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 3 && memcmp(buf, "ID3", 3) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/mpeg", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 2 && buf[0] == 0xff && (buf[1] & 0xe0) == 0xe0 && (buf[1] & 0x06) != 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/mpeg", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 2 && buf[0] == 0xff && (buf[1] & 0xf0) == 0xf0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/aac", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 12 && memcmp(buf, "RIFF", 4) == 0 && memcmp(buf + 8, "WAVE", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/wav", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 4 && memcmp(buf, "fLaC", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/flac", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 4 && memcmp(buf, "OggS", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/ogg", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 12 && memcmp(buf, "FORM", 4) == 0 && (memcmp(buf + 8, "AIFF", 4) == 0 || memcmp(buf + 8, "AIFC", 4) == 0))
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/aiff", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }
    if (buf && len >= 4 && memcmp(buf, "MThd", 4) == 0)
    {
        if (meta->mime[0] == '\0' || str_contains_ci(meta->mime, "octet-stream"))
            strncpy(meta->mime, "audio/midi", sizeof(meta->mime) - 1);
        strncpy(meta->mediaKind, "audio", sizeof(meta->mediaKind) - 1);
        return 1;
    }

    return 0;
}

static int json_find_key(const char *json, const char *key, char *out, size_t outSize)
{
    char pattern[128];
    char *p, *q;
    size_t i = 0;

    if (!json || !key || !out || outSize < 2) return 0;
    snprintf(pattern, sizeof(pattern), "\"%s\"", key);
    p = strstr((char *)json, pattern);
    if (!p) return 0;
    p += strlen(pattern);
    while (*p && *p != ':') p++;
    if (*p != ':') return 0;
    p++;
    while (*p && isspace((unsigned char)*p)) p++;

    if (*p == '"')
    {
        p++;
        q = p;
        while (*q && *q != '"' && i + 1 < outSize)
            out[i++] = *q++;
        out[i] = '\0';
        trim_inplace(out);
        return out[0] != '\0';
    }

    q = p;
    while (*q && *q != ',' && *q != '}' && !isspace((unsigned char)*q) && i + 1 < outSize)
        out[i++] = *q++;
    out[i] = '\0';
    trim_inplace(out);
    return out[0] != '\0';
}

static char *find_stream_section(const char *json, const char *codecType)
{
    char pattern[64];
    snprintf(pattern, sizeof(pattern), "\"codec_type\": \"%s\"", codecType);
    return strstr((char *)json, pattern);
}

static void parse_frame_rate(const char *fpsIn, char *fpsOut, size_t fpsOutSize)
{
    double a, b;
    if (!fpsIn || !fpsOut || fpsOutSize < 2) return;
    fpsOut[0] = '\0';
    if (sscanf(fpsIn, "%lf/%lf", &a, &b) == 2 && b != 0.0)
        snprintf(fpsOut, fpsOutSize, "%.2f", a / b);
    else
        strncpy(fpsOut, fpsIn, fpsOutSize - 1);
    fpsOut[fpsOutSize - 1] = '\0';
}

static int probe_try_acquire_slot(void)
{
    int current;
    if (gProbeThreadsMax == 0) return 0;
    for (;;)
    {
        current = gProbeActive;
        if ((unsigned int)current >= gProbeThreadsMax)
            return 0;
#ifdef WIN32
        if (InterlockedCompareExchange((volatile LONG*)&gProbeActive, current + 1, current) == current)
#else
        if (__sync_bool_compare_and_swap(&gProbeActive, current, current + 1))
#endif
            return 1;
    }
}

static void probe_release_slot(void)
{
#ifdef WIN32
    InterlockedDecrement((volatile LONG*)&gProbeActive);
#else
    __sync_sub_and_fetch(&gProbeActive, 1);
#endif
}

static int spawn_ffprobe(const char *inputPath, const char *outputPath)
{
#ifdef WIN32
    char cmd[1800];
    int rc;
    snprintf(cmd, sizeof(cmd),
             "\"%s\" -v error -print_format json -show_format -show_streams \"%s\" > \"%s\" 2>NUL",
             gFfprobePath, inputPath, outputPath);
    rc = system(cmd);
    return (rc == 0) ? 0 : -1;
#else
    pid_t pid = fork();
    if (pid < 0) return -1;
    if (pid == 0)
    {
        int outfd = open(outputPath, O_CREAT | O_TRUNC | O_WRONLY, 0600);
        int nullfd = open("/dev/null", O_WRONLY);
        char *argv[9];

        if (outfd >= 0)
        {
            dup2(outfd, STDOUT_FILENO);
            close(outfd);
        }
        if (nullfd >= 0)
        {
            dup2(nullfd, STDERR_FILENO);
            close(nullfd);
        }

        argv[0] = gFfprobePath;
        argv[1] = "-v";
        argv[2] = "error";
        argv[3] = "-print_format";
        argv[4] = "json";
        argv[5] = "-show_format";
        argv[6] = "-show_streams";
        argv[7] = (char *)inputPath;
        argv[8] = NULL;
        execvp(gFfprobePath, argv);
        _exit(127);
    }
    return (int)pid;
#endif
}

static int wait_child_timeout(pid_t pid, unsigned int timeoutMs)
{
#ifdef WIN32
    (void)timeoutMs;
    return (pid == 0) ? 0 : -1;
#else
    unsigned int waited = 0;
    int status;
    for (;;)
    {
        pid_t rc = waitpid(pid, &status, WNOHANG);
        if (rc == pid)
            return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
        if (rc < 0)
            return -1;
        if (waited >= timeoutMs)
        {
            kill(pid, SIGKILL);
            waitpid(pid, &status, 0);
            return -2;
        }
        usleep(50000);
        waited += 50;
    }
#endif
}

static void cleanup_tmp_paths(const char *inputPath, const char *outputPath)
{
    if (!gDebugKeepTmp)
    {
        if (inputPath && inputPath[0]) unlink(inputPath);
        if (outputPath && outputPath[0]) unlink(outputPath);
    }
}

static char *mysql_escape_dyn(MYSQL *db, const char *src)
{
    size_t srcLen;
    char *dst;

    if (!db)
        return NULL;
    if (!src)
        src = "";

    srcLen = strlen(src);
    dst = (char*)malloc(srcLen * 2 + 1);
    if (!dst)
        return NULL;

    mysql_real_escape_string(db, dst, src, srcLen);
    return dst;
}

static int query_get_int(MYSQL *db, const char *sql, long *value)
{
    MYSQL_RES *res;
    MYSQL_ROW row;

    if (!db || !sql || !value)
        return 0;
    if (mysql_query(db, sql) != 0)
        return 0;

    res = mysql_store_result(db);
    if (!res)
        return 0;

    row = mysql_fetch_row(res);
    if (row && row[0])
    {
        *value = atol(row[0]);
        mysql_free_result(res);
        return 1;
    }

    mysql_free_result(res);
    return 0;
}

static int ensure_media_schema(MYSQL *db)
{
    const char *q1 =
        "CREATE TABLE IF NOT EXISTS attachments_media ("
        "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,"
        "hostname VARCHAR(100) NOT NULL,"
        "page VARCHAR(255) NOT NULL,"
        "url_id BIGINT UNSIGNED NULL,"
        "media_kind VARCHAR(16) NOT NULL,"
        "mime VARCHAR(128) NULL,"
        "title VARCHAR(512) NULL,"
        "descr VARCHAR(1024) NULL,"
        "descr_source VARCHAR(16) NULL,"
        "ocr_text MEDIUMTEXT NULL,"
        "author VARCHAR(255) NULL,"
        "software VARCHAR(255) NULL,"
        "artist VARCHAR(255) NULL,"
        "album VARCHAR(255) NULL,"
        "video_codec VARCHAR(64) NULL,"
        "audio_codec VARCHAR(64) NULL,"
        "duration_raw VARCHAR(64) NULL,"
        "bit_rate VARCHAR(64) NULL,"
        "width INT NULL,"
        "height INT NULL,"
        "fps VARCHAR(32) NULL,"
        "sample_rate VARCHAR(64) NULL,"
        "channels VARCHAR(32) NULL,"
        "size_bytes BIGINT UNSIGNED NULL,"
        "http_etag VARCHAR(255) NULL,"
        "http_last_modified VARCHAR(255) NULL,"
        "first_seen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
        "last_seen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
        "PRIMARY KEY (id),"
        "UNIQUE KEY uq_media_url (hostname, page),"
        "KEY idx_media_kind (media_kind),"
        "KEY idx_mime (mime),"
        "KEY idx_url_id (url_id)"
        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";

    if (!db)
        return 0;
    if (gMediaSchemaReady)
        return 1;
    if (mysql_query(db, q1) != 0)
        return 0;
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS descr_source VARCHAR(16) NULL");
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS ocr_text MEDIUMTEXT NULL");
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS author VARCHAR(255) NULL");
    mysql_query(db, "ALTER TABLE attachments_media ADD COLUMN IF NOT EXISTS software VARCHAR(255) NULL");
    gMediaSchemaReady = 1;
    return 1;
}

static int save_media_meta(FUNCTION_ARGUMENT *arg, const MEDIA_META *meta)
{
    MYSQL *db;
    char sql[8192];
    char urlExpr[32];
    char *eHost, *ePage, *eKind, *eMime, *eTitle, *eDescr, *eArtist, *eAlbum;
    char *eVCodec, *eACodec, *eDuration, *eBitRate, *eFps, *eSample, *eChannels;
    char *eEtag, *eLm;
    long urlId = 0;

    if (!arg || !arg->hostInfo || !arg->mysqlDB2 || !meta)
        return 0;

    db = (MYSQL*)arg->mysqlDB2;
    if (!ensure_media_schema(db))
        return 0;

    eHost = mysql_escape_dyn(db, arg->hostInfo->Host);
    ePage = mysql_escape_dyn(db, arg->hostInfo->Page);
    eKind = mysql_escape_dyn(db, meta->mediaKind);
    eMime = mysql_escape_dyn(db, meta->mime);
    eTitle = mysql_escape_dyn(db, meta->title);
    eDescr = mysql_escape_dyn(db, meta->description);
    eArtist = mysql_escape_dyn(db, meta->artist);
    eAlbum = mysql_escape_dyn(db, meta->album);
    eVCodec = mysql_escape_dyn(db, meta->videoCodec);
    eACodec = mysql_escape_dyn(db, meta->audioCodec);
    eDuration = mysql_escape_dyn(db, meta->duration);
    eBitRate = mysql_escape_dyn(db, meta->bitRate);
    eFps = mysql_escape_dyn(db, meta->fps);
    eSample = mysql_escape_dyn(db, meta->sampleRate);
    eChannels = mysql_escape_dyn(db, meta->channels);
    eEtag = mysql_escape_dyn(db, arg->hostInfo->HttpETag);
    eLm = mysql_escape_dyn(db, arg->hostInfo->HttpLastModified);

    snprintf(sql, sizeof(sql),
             "SELECT id FROM pagelist WHERE hostname='%s' AND page='%s' LIMIT 1",
             eHost, ePage);
    query_get_int(db, sql, &urlId);

    if (urlId > 0)
        snprintf(urlExpr, sizeof(urlExpr), "%ld", urlId);
    else
        strcpy(urlExpr, "NULL");

    snprintf(sql, sizeof(sql),
             "INSERT INTO attachments_media("
             "hostname,page,url_id,media_kind,mime,title,descr,artist,album,video_codec,audio_codec,duration_raw,bit_rate,width,height,fps,sample_rate,channels,size_bytes,http_etag,http_last_modified"
             ") VALUES ("
             "'%s','%s',%s,'%s','%s','%s','%s','%s','%s','%s','%s','%s','%s',%u,%u,'%s','%s','%s',%u,'%s','%s'"
             ") ON DUPLICATE KEY UPDATE "
             "url_id=VALUES(url_id),media_kind=VALUES(media_kind),mime=VALUES(mime),title=VALUES(title),descr=VALUES(descr),artist=VALUES(artist),album=VALUES(album),"
             "video_codec=VALUES(video_codec),audio_codec=VALUES(audio_codec),duration_raw=VALUES(duration_raw),bit_rate=VALUES(bit_rate),width=VALUES(width),height=VALUES(height),"
             "fps=VALUES(fps),sample_rate=VALUES(sample_rate),channels=VALUES(channels),size_bytes=VALUES(size_bytes),http_etag=VALUES(http_etag),http_last_modified=VALUES(http_last_modified),last_seen=CURRENT_TIMESTAMP",
             eHost,
             ePage,
             urlExpr,
             eKind,
             eMime,
             eTitle,
             eDescr,
             eArtist,
             eAlbum,
             eVCodec,
             eACodec,
             eDuration,
             eBitRate,
             meta->width,
             meta->height,
             eFps,
             eSample,
             eChannels,
             meta->sizeBytes,
             eEtag,
             eLm);

    mysql_query(db, sql);

    if (eHost) free(eHost);
    if (ePage) free(ePage);
    if (eKind) free(eKind);
    if (eMime) free(eMime);
    if (eTitle) free(eTitle);
    if (eDescr) free(eDescr);
    if (eArtist) free(eArtist);
    if (eAlbum) free(eAlbum);
    if (eVCodec) free(eVCodec);
    if (eACodec) free(eACodec);
    if (eDuration) free(eDuration);
    if (eBitRate) free(eBitRate);
    if (eFps) free(eFps);
    if (eSample) free(eSample);
    if (eChannels) free(eChannels);
    if (eEtag) free(eEtag);
    if (eLm) free(eLm);
    return 1;
}

static int probe_media(const struct functArg *arg, MEDIA_META *meta)
{
    char inTemplate[] = OWS_VIDEO_TMP_IN_TEMPLATE;
    char inputPath[512];
    char outputPath[512];
    char json[MAXPACKETBUFSIZE];
    char tmp[128];
    char dbg[1024];
    FILE *fp;
    int fd;
    int slotAcquired = 0;
    int childPid;
    int waitRc;
    char *videoSec;
    char *audioSec;
    const char *ext;

    if (!arg || !meta) return 0;
    if (!gProbeEnabled || gFfprobePath[0] == '\0') return 0;
    if (is_stream_playlist(arg->hostInfo) && gSkipStreaming)
    {
        DEBUG_LOG("media probe skipped streaming playlist");
        return 0;
    }
    if (arg->htmlLength == 0 || arg->htmlLength > gProbeMaxBytes)
    {
        snprintf(dbg, sizeof(dbg), "media probe skipped size page=%.255s bytes=%u max=%u", arg->hostInfo->Page, arg->htmlLength, gProbeMaxBytes);
        DEBUG_LOG(dbg);
        return 0;
    }
    if (!probe_try_acquire_slot())
    {
        snprintf(dbg, sizeof(dbg), "media probe skipped throttle page=%.255s active=%d max=%u", arg->hostInfo->Page, (int)gProbeActive, gProbeThreadsMax);
        DEBUG_LOG(dbg);
        return 0;
    }
    slotAcquired = 1;

    inputPath[0] = '\0';
    outputPath[0] = '\0';

    fd = mkstemp(inTemplate);
    if (fd < 0) goto done;
    close(fd);

    if (strstr(meta->mime, "video/mp4")) ext = ".mp4";
    else if (strstr(meta->mime, "video/webm")) ext = ".webm";
    else if (strstr(meta->mime, "audio/mpeg")) ext = ".mp3";
    else if (strstr(meta->mime, "audio/mp4")) ext = ".m4a";
    else ext = ".media";

    snprintf(inputPath, sizeof(inputPath), "%s%s", inTemplate, ext);
    if (rename(inTemplate, inputPath) != 0)
    {
        unlink(inTemplate);
        goto done;
    }

    fp = fopen(inputPath, "wb");
    if (!fp) goto done;
    fwrite(arg->html, 1, arg->htmlLength, fp);
    fclose(fp);

    snprintf(outputPath, sizeof(outputPath), "%s.json", inputPath);
    childPid = spawn_ffprobe(inputPath, outputPath);
    if (childPid < 0) goto done;
    waitRc = wait_child_timeout((pid_t)childPid, gProbeTimeoutMs);
    if (waitRc != 0)
    {
        snprintf(dbg, sizeof(dbg), "media probe failed page=%.255s exit=%d", arg->hostInfo->Page, waitRc);
        DEBUG_LOG(dbg);
        goto done;
    }
    if (!read_file_into_buffer(outputPath, json, sizeof(json)))
        goto done;

    if (json_find_key(json, "duration", meta->duration, sizeof(meta->duration))) {}
    if (json_find_key(json, "size", tmp, sizeof(tmp))) meta->sizeBytes = (unsigned int)strtoul(tmp, NULL, 10);
    if (json_find_key(json, "bit_rate", meta->bitRate, sizeof(meta->bitRate))) {}
    if (json_find_key(json, "title", meta->title, sizeof(meta->title))) {}
    if (json_find_key(json, "comment", meta->description, sizeof(meta->description))) {}
    if (json_find_key(json, "artist", meta->artist, sizeof(meta->artist))) {}
    if (json_find_key(json, "album", meta->album, sizeof(meta->album))) {}

    videoSec = find_stream_section(json, "video");
    if (videoSec)
    {
        char rateRaw[64];
        json_find_key(videoSec, "codec_name", meta->videoCodec, sizeof(meta->videoCodec));
        if (json_find_key(videoSec, "width", tmp, sizeof(tmp))) meta->width = (unsigned int)strtoul(tmp, NULL, 10);
        if (json_find_key(videoSec, "height", tmp, sizeof(tmp))) meta->height = (unsigned int)strtoul(tmp, NULL, 10);
        if (json_find_key(videoSec, "avg_frame_rate", rateRaw, sizeof(rateRaw)))
            parse_frame_rate(rateRaw, meta->fps, sizeof(meta->fps));
    }

    audioSec = find_stream_section(json, "audio");
    if (audioSec)
    {
        json_find_key(audioSec, "codec_name", meta->audioCodec, sizeof(meta->audioCodec));
        json_find_key(audioSec, "sample_rate", meta->sampleRate, sizeof(meta->sampleRate));
        json_find_key(audioSec, "channels", meta->channels, sizeof(meta->channels));
    }

    snprintf(dbg, sizeof(dbg), "media probe ok page=%.255s kind=%.31s duration=%.31s", arg->hostInfo->Page, meta->mediaKind, meta->duration);
    DEBUG_LOG(dbg);

done:
    cleanup_tmp_paths(inputPath, outputPath);
    if (slotAcquired)
        probe_release_slot();
    return 1;
}

#ifdef WIN32
extern __declspec(dllexport)
#endif
int modFilter(struct functArg *arg)
{
    MEDIA_META meta;
    char filename[512];
    char bestTitle[MAXDESCRIPTIONSIZE];
    char text[MAXPACKETBUFSIZE];

    if (!arg || !arg->hostInfo) return 0;
    if (arg->hostInfo->type != 4) return 1;

    memset(&meta, 0, sizeof(meta));
    if (!detect_media(arg, &meta))
        return 1;

    basename_from_path(arg->hostInfo, filename, sizeof(filename));

    if (is_stream_playlist(arg->hostInfo))
        strncpy(meta.mediaKind, "stream", sizeof(meta.mediaKind) - 1);
    else
        probe_media(arg, &meta);

    bestTitle[0] = '\0';
    if (meta.title[0] != '\0')
        strncpy(bestTitle, meta.title, sizeof(bestTitle) - 1);
    else if (filename[0] != '\0')
        strncpy(bestTitle, filename, sizeof(bestTitle) - 1);
    bestTitle[sizeof(bestTitle) - 1] = '\0';

    memset(text, 0, sizeof(text));
    append_field(text, sizeof(text), "media", meta.mediaKind[0] ? meta.mediaKind : "asset");
    append_field(text, sizeof(text), "mime", meta.mime);
    append_field(text, sizeof(text), "title", meta.title);
    append_field(text, sizeof(text), "description", meta.description);
    append_field(text, sizeof(text), "artist", meta.artist);
    append_field(text, sizeof(text), "album", meta.album);
    append_field(text, sizeof(text), "video_codec", meta.videoCodec);
    append_field(text, sizeof(text), "audio_codec", meta.audioCodec);
    append_field(text, sizeof(text), "duration", meta.duration);
    append_field(text, sizeof(text), "bit_rate", meta.bitRate);
    append_field(text, sizeof(text), "fps", meta.fps);
    append_field(text, sizeof(text), "sample_rate", meta.sampleRate);
    append_field(text, sizeof(text), "channels", meta.channels);
    append_number_field(text, sizeof(text), "width", meta.width);
    append_number_field(text, sizeof(text), "height", meta.height);
    append_number_field(text, sizeof(text), "size_bytes", meta.sizeBytes);
    if (is_stream_playlist(arg->hostInfo))
        append_field(text, sizeof(text), "stream_playlist", "1");

    if (bestTitle[0] != '\0')
    {
        strncpy(arg->hostInfo->Description, bestTitle, MAXDESCRIPTIONSIZE - 1);
        arg->hostInfo->Description[MAXDESCRIPTIONSIZE - 1] = '\0';
    }

    strncpy(arg->text, text, MAXPACKETBUFSIZE - 1);
    arg->text[MAXPACKETBUFSIZE - 1] = '\0';
    arg->textLength = strlen(arg->text);
    save_media_meta(arg, &meta);
    return 1;
}

#ifdef WIN32
extern __declspec(dllexport)
#endif
int modInitFilter(char *hostname, char *error)
{
    FILE *pF;
    char line[600];

    (void)hostname;
    if (error) error[0] = '\0';

    gProbeEnabled = 1;
    strncpy(gFfprobePath, OWS_DEFAULT_FFPROBE, sizeof(gFfprobePath) - 1);
    gFfprobePath[sizeof(gFfprobePath) - 1] = '\0';
    gProbeMaxBytes = 50000000;
    gProbeTimeoutMs = 8000;
    gProbeThreadsMax = 1;
    gSkipStreaming = 1;
    gDebugKeepTmp = 0;

    pF = ows_fopen_config("mod_video.conf", "r", NULL, 0);
    if (!pF)
        return 1;

    while (fgets(line, sizeof(line) - 1, pF))
    {
        char parsed[600];
        strncpy(parsed, line, sizeof(parsed) - 1);
        parsed[sizeof(parsed) - 1] = '\0';
        trim_inplace(parsed);
        if (parsed[0] == '\0' || parsed[0] == '#')
            continue;

        if (starts_with(parsed, "enabled="))
            gProbeEnabled = atoi(parsed + 8) ? 1 : 0;
        else if (starts_with(parsed, "ffprobe="))
        {
            strncpy(gFfprobePath, parsed + 8, sizeof(gFfprobePath) - 1);
            gFfprobePath[sizeof(gFfprobePath) - 1] = '\0';
        }
        else if (starts_with(parsed, "max_bytes="))
            gProbeMaxBytes = (unsigned int)strtoul(parsed + 10, NULL, 10);
        else if (starts_with(parsed, "timeout_ms="))
            gProbeTimeoutMs = (unsigned int)strtoul(parsed + 11, NULL, 10);
        else if (starts_with(parsed, "probe_threads_max="))
            gProbeThreadsMax = (unsigned int)strtoul(parsed + 18, NULL, 10);
        else if (starts_with(parsed, "skip_streaming="))
            gSkipStreaming = atoi(parsed + 15) ? 1 : 0;
        else if (starts_with(parsed, "debug_keep_tmp="))
            gDebugKeepTmp = atoi(parsed + 15) ? 1 : 0;
    }

    fclose(pF);
    if (gProbeThreadsMax < 1) gProbeThreadsMax = 1;
    if (gProbeThreadsMax > 8) gProbeThreadsMax = 8;
    if (gProbeTimeoutMs < 100) gProbeTimeoutMs = 100;
    if (gProbeMaxBytes < 1024) gProbeMaxBytes = 1024;
    return 1;
}
