"""Flask service for the Seal challenge.

Routes:
    GET  /              Catalog listing + upload form
    GET  /videos/<id>   Stream a watermarked clip from the catalog
    POST /upload        Accept a video, extract the watermark, gate on admin name
    GET  /health        Liveness probe

There is intentionally no auth, no username field, and no listing of who
"owns" each catalog clip -- identity is whatever videoseal extracts from a
submitted MP4.
"""

from __future__ import annotations

import concurrent.futures
import hmac
import json
import logging
import os
import tempfile
import threading
from pathlib import Path
from typing import Optional

from flask import (
    Flask, Response, abort, render_template, request,
    send_from_directory, url_for,
)
from werkzeug.exceptions import RequestEntityTooLarge
from werkzeug.utils import secure_filename

from . import config, payload_codec

log = logging.getLogger("seal")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")

_INFERENCE_SEMAPHORE = threading.Semaphore(config.MAX_CONCURRENT_INFERENCES)
_INFERENCE_POOL = concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_CONCURRENT_INFERENCES)


def _list_catalog() -> list[str]:
    if not config.CATALOG_DIR.exists():
        return []
    return sorted(p.name for p in config.CATALOG_DIR.iterdir() if p.suffix.lower() == ".mp4")


def _ext_ok(filename: str) -> bool:
    return Path(filename).suffix.lower() in config.ALLOWED_EXT


def _probe_bounds(path: Path) -> Optional[str]:
    """Return None if the file is within bounds, else an outcome string."""
    try:
        import av
    except ImportError:
        log.warning("pyav not installed; skipping bounds check")
        return None
    try:
        with av.open(str(path)) as container:
            streams = [s for s in container.streams if s.type == "video"]
            if not streams:
                return "unreadable"
            s = streams[0]
            duration_s = float(container.duration) / 1e6 if container.duration else 0.0
            if duration_s <= 0 and s.duration and s.time_base:
                duration_s = float(s.duration * s.time_base)
            if duration_s <= 0:
                return "unreadable"
            if duration_s > config.MAX_DURATION_SEC:
                return "bounds"
            width, height = s.codec_context.width or 0, s.codec_context.height or 0
            if max(width, height) > config.MAX_LONGEST_SIDE:
                return "bounds"
    except Exception:
        return "unreadable"
    return None


def _do_extract(path: Path) -> Optional[str]:
    from . import videoseal_helper
    bits = videoseal_helper.extract_payload(path)
    return payload_codec.decode_strict(bits) or payload_codec.decode_fuzzy(bits, config.HAMMING_TOL_BITS)


def _check_manifest_consistency() -> None:
    """Log a loud warning if ADMIN_USERNAME is not in the catalog manifest --
    that would mean the puzzle is unsolvable."""
    manifest_path = config.CATALOG_DIR.parent / "catalog_manifest.json"
    if not manifest_path.exists():
        log.warning("catalog_manifest.json missing; cannot verify admin presence in catalog")
        return
    try:
        manifest = json.loads(manifest_path.read_text())
    except json.JSONDecodeError:
        log.warning("catalog_manifest.json unreadable; cannot verify admin presence")
        return
    if manifest.get("admin") != config.ADMIN_USERNAME:
        log.error("admin mismatch! manifest=%r runtime=%r -- puzzle may be unsolvable",
                  manifest.get("admin"), config.ADMIN_USERNAME)


def create_app() -> Flask:
    app = Flask(__name__, template_folder="templates", static_folder="static")
    app.config["MAX_CONTENT_LENGTH"] = config.MAX_UPLOAD_BYTES
    app.url_map.strict_slashes = False

    _check_manifest_consistency()

    @app.errorhandler(RequestEntityTooLarge)
    def _too_big(_e):
        return render_template("result.html", outcome="too_big", flag=None, name=None), 413

    @app.get("/")
    def index():
        videos = _list_catalog()
        return render_template("index.html", videos=videos, count=len(videos),
                               admin=config.ADMIN_USERNAME)

    @app.get("/videos/<vid_id>")
    def video(vid_id: str):
        safe = secure_filename(vid_id)
        if safe != vid_id or not safe.endswith(".mp4"):
            abort(404)
        resp = send_from_directory(config.CATALOG_DIR, safe, mimetype="video/mp4")
        resp.headers["X-Content-Signed-By"] = "videoseal/v1.0/256bits"
        return resp

    @app.post("/upload")
    def upload():
        f = request.files.get("video")
        if not f or not f.filename:
            return render_template("result.html", outcome="no_file", flag=None, name=None), 400
        if not _ext_ok(f.filename):
            return render_template("result.html", outcome="bad_ext", flag=None, name=None), 400

        ext = Path(f.filename).suffix.lower()
        config.UPLOAD_TMP_DIR.mkdir(parents=True, exist_ok=True)
        tmp = tempfile.NamedTemporaryFile(dir=str(config.UPLOAD_TMP_DIR), suffix=ext, delete=False)
        try:
            f.save(tmp.name)
            tmp.close()
            path = Path(tmp.name)

            bounds = _probe_bounds(path)
            if bounds is not None:
                return render_template("result.html", outcome=bounds, flag=None, name=None), 400

            if not _INFERENCE_SEMAPHORE.acquire(timeout=10):
                return render_template("result.html", outcome="unreadable", flag=None, name=None), 503
            try:
                future = _INFERENCE_POOL.submit(_do_extract, path)
                try:
                    name = future.result(timeout=config.INFERENCE_TIMEOUT_SEC)
                except concurrent.futures.TimeoutError:
                    log.info("upload outcome=unreadable reason=timeout")
                    return render_template("result.html", outcome="unreadable", flag=None, name=None), 504
                except Exception:
                    log.exception("upload outcome=unreadable reason=exception")
                    return render_template("result.html", outcome="unreadable", flag=None, name=None), 400
            finally:
                _INFERENCE_SEMAPHORE.release()

            if name is None:
                log.info("upload outcome=no_watermark")
                return render_template("result.html", outcome="no_watermark", flag=None, name=None)

            if hmac.compare_digest(name.encode("ascii"), config.ADMIN_USERNAME.encode("ascii")):
                log.info("upload outcome=admin_match")
                return render_template("result.html", outcome="admin_match",
                                       flag=config.FLAG, name=config.ADMIN_USERNAME)

            log.info("upload outcome=welcome")
            return render_template("result.html", outcome="welcome", flag=None, name=name)
        finally:
            try:
                os.unlink(tmp.name)
            except OSError:
                pass

    @app.get("/health")
    def health():
        return Response("ok\n", mimetype="text/plain")

    return app
