#!/usr/bin/env python3
import base64
import json
import mimetypes
import os
import re
import shutil
import subprocess
import sys
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, quote, unquote, urlparse

HOST = "127.0.0.1"
PORTS = [17660, 17661, 17662, 17663, 17664, 17665, 17666, 17667, 17668, 17669, 17670]
ROOT = Path(__file__).resolve().parent
OUTPUT_ROOT = ROOT / "outputs"
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)

def find_ffmpeg():
    try:
        import imageio_ffmpeg
        path = imageio_ffmpeg.get_ffmpeg_exe()
        if path and Path(path).exists():
            return path
    except Exception:
        pass
    return shutil.which("ffmpeg") or ""

def has_cv2():
    try:
        import cv2  # noqa: F401
        return True
    except Exception:
        return False

def duration_with_cv2(path):
    try:
        import cv2
        cap = cv2.VideoCapture(str(path))
        fps = cap.get(cv2.CAP_PROP_FPS) or 0
        frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0
        cap.release()
        if fps > 0 and frames > 0:
            return float(frames / fps)
    except Exception:
        return 0.0
    return 0.0

def duration_with_ffmpeg(ffmpeg, path):
    try:
        proc = subprocess.run([ffmpeg, "-hide_banner", "-i", str(path)], capture_output=True, text=True, timeout=30)
        text = (proc.stderr or "") + (proc.stdout or "")
        match = re.search(r"Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)", text)
        if match:
            return int(match.group(1)) * 3600 + int(match.group(2)) * 60 + float(match.group(3))
    except Exception:
        return 0.0
    return 0.0

def get_duration(ffmpeg, path):
    return duration_with_cv2(path) or duration_with_ffmpeg(ffmpeg, path)

def clamp_float(value, default=0.0):
    try:
        number = float(value)
        if number >= 0:
            return number
    except Exception:
        pass
    return default

def clamp_int(value, default=0):
    try:
        number = int(float(value))
        if number >= 0:
            return number
    except Exception:
        pass
    return default

def build_segments(duration, query):
    mode = (query.get("mode") or ["fixed_count"])[0]
    target_count = max(1, min(200, clamp_int((query.get("target_count") or ["0"])[0], 0)))
    fixed_duration = clamp_float((query.get("fixed_duration_seconds") or ["0"])[0], 0.0)
    min_duration = clamp_float((query.get("min_duration_seconds") or ["0"])[0], 0.0)
    max_duration = clamp_float((query.get("max_duration_seconds") or ["0"])[0], 0.0)
    if duration <= 0:
        duration = fixed_duration * target_count if fixed_duration and target_count else 0
    if mode == "fixed_duration" and fixed_duration > 0 and duration > 0:
        count = max(1, min(200, int((duration + fixed_duration - 0.001) // fixed_duration)))
        step = fixed_duration
    elif target_count > 0 and duration > 0:
        count = target_count
        step = duration / count
    else:
        count = target_count or 1
        step = fixed_duration or 8.0
    rows = []
    for index in range(count):
        start = index * step
        if duration > 0 and start >= duration:
            break
        end = min(duration, start + step) if duration > 0 else start + step
        if min_duration and end - start < min_duration and duration > 0 and index + 1 < count:
            continue
        if max_duration and end - start > max_duration:
            end = start + max_duration
        rows.append((start, max(start + 0.1, end)))
    return rows or [(0.0, duration or fixed_duration or 8.0)]

def cut_segment(ffmpeg, source, target, start, end):
    duration = max(0.1, end - start)
    common = [ffmpeg, "-y", "-hide_banner", "-loglevel", "error", "-ss", f"{start:.3f}", "-t", f"{duration:.3f}", "-i", str(source)]
    copy_cmd = common + ["-c", "copy", "-movflags", "+faststart", str(target)]
    proc = subprocess.run(copy_cmd, capture_output=True, text=True, timeout=max(60, int(duration) + 60))
    if proc.returncode == 0 and target.exists() and target.stat().st_size > 0:
        return
    encode_cmd = common + ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-c:a", "aac", "-movflags", "+faststart", str(target)]
    proc = subprocess.run(encode_cmd, capture_output=True, text=True, timeout=max(120, int(duration) + 120))
    if proc.returncode != 0:
        raise RuntimeError((proc.stderr or proc.stdout or "ffmpeg failed").strip()[:500])

class Handler(BaseHTTPRequestHandler):
    server_version = "QDALocalVideoHelper/1.0"

    def end_headers(self):
        origin = self.headers.get("Origin") or "*"
        self.send_header("Access-Control-Allow-Origin", origin)
        self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type,X-QDA-Local-Segmenter,X-QDA-File-Name")
        self.send_header("Cache-Control", "no-store")
        super().end_headers()

    def write_json(self, status, payload):
        data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_OPTIONS(self):
        self.send_response(204)
        self.end_headers()

    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path == "/health":
            ffmpeg = find_ffmpeg()
            port = int(self.server.server_address[1])
            self.write_json(200, {"ok": bool(ffmpeg), "ffmpeg": bool(ffmpeg), "opencv": has_cv2(), "port": port})
            return
        if parsed.path.startswith("/files/"):
            rel = Path(unquote(parsed.path.replace("/files/", "", 1)))
            target = (OUTPUT_ROOT / rel).resolve()
            if OUTPUT_ROOT.resolve() not in target.parents:
                self.write_json(403, {"ok": False, "error": "forbidden"})
                return
            if not target.exists() or not target.is_file():
                self.write_json(404, {"ok": False, "error": "not found"})
                return
            mime = mimetypes.guess_type(str(target))[0] or "application/octet-stream"
            self.send_response(200)
            self.send_header("Content-Type", mime)
            self.send_header("Content-Length", str(target.stat().st_size))
            self.end_headers()
            with target.open("rb") as handle:
                shutil.copyfileobj(handle, self.wfile)
            return
        self.write_json(404, {"ok": False, "error": "unknown route"})

    def do_POST(self):
        parsed = urlparse(self.path)
        if parsed.path != "/api/video/segment":
            self.write_json(404, {"ok": False, "error": "unknown route"})
            return
        ffmpeg = find_ffmpeg()
        if not ffmpeg:
            self.write_json(503, {"ok": False, "error": "ffmpeg is not available"})
            return
        length = int(self.headers.get("Content-Length") or "0")
        if length <= 0:
            self.write_json(400, {"ok": False, "error": "empty video body"})
            return
        task_id = "local_" + uuid.uuid4().hex[:12]
        task_dir = OUTPUT_ROOT / task_id
        task_dir.mkdir(parents=True, exist_ok=True)
        source_name = self.headers.get("X-QDA-File-Name") or "input.mp4"
        suffix = Path(source_name).suffix or ".mp4"
        source = task_dir / ("input" + suffix)
        with source.open("wb") as handle:
            remaining = length
            while remaining > 0:
                chunk = self.rfile.read(min(1024 * 1024, remaining))
                if not chunk:
                    break
                handle.write(chunk)
                remaining -= len(chunk)
        query = parse_qs(parsed.query)
        duration = get_duration(ffmpeg, source)
        segments = []
        for index, (start, end) in enumerate(build_segments(duration, query), start=1):
            file_name = f"segment_{index:03d}.mp4"
            target = task_dir / file_name
            cut_segment(ffmpeg, source, target, start, end)
            rel_url = f"/files/{quote(task_id)}/{quote(file_name)}"
            port = int(self.server.server_address[1])
            url = f"http://{HOST}:{port}{rel_url}"
            segments.append({
                "id": f"S{index:02d}",
                "label": f"Segment {index}",
                "url": url,
                "inline_url": url,
                "download_url": url,
                "start": start,
                "end": end,
                "duration": max(0.0, end - start),
                "actual_duration": max(0.0, end - start),
                "local_only": True,
                "cloud_upload": False
            })
        self.write_json(200, {
            "ok": True,
            "task_id": task_id,
            "segment_count": len(segments),
            "segments": segments,
            "segment_infos": segments,
            "video_list": [item["url"] for item in segments],
            "video_info": {"duration": duration},
            "scene_detection_method": (query.get("scene_detection_method") or ["ffmpeg"])[0],
            "local_only": True,
            "cloud_upload": False,
            "execution_boundary": "user_local_port"
        })

def main():
    last_error = None
    for port in PORTS:
        try:
            server = ThreadingHTTPServer((HOST, port), Handler)
            print(f"QDA local video helper listening on http://{HOST}:{port}", flush=True)
            server.serve_forever()
            return
        except OSError as exc:
            last_error = exc
            continue
    raise SystemExit(f"Could not bind any QDA helper port: {last_error}")

if __name__ == "__main__":
    main()
