from __future__ import annotations

import argparse
import json
import mimetypes
import re
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

from analyze8780_repository import AnalyzeRepository

VERSION = "2.3.8.232"
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8780


class AnalyzeHandler(BaseHTTPRequestHandler):
    repository: AnalyzeRepository
    web_root: Path

    def log_message(self, fmt: str, *args: object) -> None:
        print(f"[Analyze8780] {self.address_string()} {fmt % args}")

    def _cors(self) -> None:
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.send_header("Cache-Control", "no-store")

    def _json(self, data: object, status: int = 200) -> None:
        raw = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(raw)))
        self._cors()
        self.end_headers()
        self.wfile.write(raw)

    def _body_json(self) -> dict:
        length = int(self.headers.get("Content-Length") or 0)
        raw = self.rfile.read(length) if length else b"{}"
        value = json.loads(raw.decode("utf-8"))
        if not isinstance(value, dict):
            raise ValueError("JSON object required")
        return value

    def _static(self, relative: str) -> None:
        rel = relative.strip("/") or "index.html"
        target = (self.web_root / rel).resolve()
        if self.web_root not in target.parents and target != self.web_root:
            self._json({"ok": False, "error": "invalid_path"}, 400)
            return
        if not target.is_file():
            self._json({"ok": False, "error": "not_found"}, 404)
            return
        raw = target.read_bytes()
        content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream"
        self.send_response(200)
        self.send_header("Content-Type", content_type + ("; charset=utf-8" if content_type.startswith("text/") else ""))
        self.send_header("Content-Length", str(len(raw)))
        self._cors()
        self.end_headers()
        self.wfile.write(raw)

    @staticmethod
    def _int(params: dict[str, list[str]], key: str, default: int) -> int:
        try:
            return int((params.get(key) or [default])[0])
        except (TypeError, ValueError):
            return default

    def do_OPTIONS(self) -> None:
        self.send_response(204)
        self._cors()
        self.end_headers()

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        path = parsed.path
        params = parse_qs(parsed.query)
        try:
            if path == "/api/health":
                self._json(self.repository.health())
                return
            if path == "/api/accounts":
                self._json(self.repository.accounts(
                    page=self._int(params, "page", 1),
                    page_size=self._int(params, "page_size", 50),
                    include_deleted=(params.get("include_deleted") or ["0"])[0] == "1",
                ))
                return
            if path == "/api/collection-accounts":
                self._json(self.repository.collection_accounts())
                return
            if path == "/api/deleted/accounts":
                self._json({"ok": True, "items": self.repository.deleted_accounts()})
                return
            match = re.fullmatch(r"/api/accounts/(\d+)/posts", path)
            if match:
                self._json(self.repository.account_posts(
                    int(match.group(1)),
                    page=self._int(params, "page", 1),
                    page_size=self._int(params, "page_size", 50),
                    sort_by=(params.get("sort_by") or ["views"])[0],
                ))
                return
            match = re.fullmatch(r"/api/posts/(\d+)/stats", path)
            if match:
                self._json({"ok": True, "items": self.repository.post_stats(int(match.group(1)), limit=self._int(params, "limit", 144))})
                return
            match = re.fullmatch(r"/api/posts/(\d+)", path)
            if match:
                item = self.repository.post(int(match.group(1)))
                self._json({"ok": bool(item), "item": item}, 200 if item else 404)
                return
            if path == "/" or path == "/index.htm" or path == "/index.html":
                self._static("index.html")
                return
            if path.startswith("/static/"):
                self._static(path[len("/static/"):])
                return
            self._json({"ok": False, "error": "not_found"}, 404)
        except Exception as exc:
            self._json({"ok": False, "error": str(exc)}, 400)


    def do_PUT(self) -> None:
        parsed = urlparse(self.path)
        try:
            if parsed.path == "/api/collection-accounts":
                self._json(self.repository.replace_collection_accounts(self._body_json()))
                return
            self._json({"ok": False, "error": "not_found"}, 404)
        except Exception as exc:
            self._json({"ok": False, "error": str(exc)}, 400)

    def do_POST(self) -> None:
        parsed = urlparse(self.path)
        try:
            if parsed.path == "/api/observations":
                result = self.repository.save_observation(self._body_json())
                self._json({"ok": True, **result}, HTTPStatus.CREATED)
                return
            self._json({"ok": False, "error": "not_found"}, 404)
        except Exception as exc:
            self._json({"ok": False, "error": str(exc)}, 400)

    def do_DELETE(self) -> None:
        path = urlparse(self.path).path
        try:
            match = re.fullmatch(r"/api/accounts/(\d+)", path)
            if match:
                self._json({"ok": True, **self.repository.delete_account(int(match.group(1)))})
                return
            match = re.fullmatch(r"/api/posts/(\d+)", path)
            if match:
                self._json({"ok": True, **self.repository.delete_post(int(match.group(1)))})
                return
            self._json({"ok": False, "error": "not_found"}, 404)
        except Exception as exc:
            self._json({"ok": False, "error": str(exc)}, 400)


def build_server(st_root: Path, db_path: Path | None, host: str, port: int) -> ThreadingHTTPServer:
    repo = AnalyzeRepository(st_root, db_path)
    handler = type("BoundAnalyzeHandler", (AnalyzeHandler,), {
        "repository": repo,
        "web_root": Path(__file__).resolve().parent / "web",
    })
    return ThreadingHTTPServer((host, port), handler)


def main() -> int:
    parser = argparse.ArgumentParser(description="sort local analysis service on port 8780")
    parser.add_argument("--host", default=DEFAULT_HOST)
    parser.add_argument("--port", type=int, default=DEFAULT_PORT)
    parser.add_argument("--st-root", default=str(Path(__file__).resolve().parents[1]))
    parser.add_argument("--db")
    args = parser.parse_args()
    server = build_server(Path(args.st_root), Path(args.db) if args.db else None, args.host, args.port)
    print(json.dumps({"overall": "OK", "service": "Analyze8780", "version": VERSION, "url": f"http://{args.host}:{args.port}/"}, ensure_ascii=False))
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
