from __future__ import annotations

import json
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, unquote, urlsplit

from services.api.local_api import LocalPetApi
from services.api.state_guards import StateTransitionError


def make_handler(*, api: LocalPetApi, web_root: Path) -> type[SimpleHTTPRequestHandler]:
    class LocalApiHandler(SimpleHTTPRequestHandler):
        def __init__(self, *args: Any, **kwargs: Any) -> None:
            super().__init__(*args, directory=str(web_root), **kwargs)

        def end_headers(self) -> None:
            self.send_header("Cache-Control", "no-store")
            super().end_headers()

        def do_GET(self) -> None:
            try:
                if urlsplit(self.path).path.startswith("/api/"):
                    status, payload = dispatch_api_request(api=api, method="GET", path=self.path, payload={})
                    self._send_json(payload, status=HTTPStatus(status))
                    return
                super().do_GET()
            except Exception as exc:
                self._send_error_json(exc)

        def do_POST(self) -> None:
            try:
                payload = self._read_stripe_webhook_payload() if urlsplit(self.path).path == "/api/cloud/webhooks/stripe" else self._read_payload()
                status, response_payload = dispatch_api_request(api=api, method="POST", path=self.path, payload=payload)
                self._send_json(response_payload, status=HTTPStatus(status))
            except Exception as exc:
                self._send_error_json(exc)

        def do_DELETE(self) -> None:
            try:
                payload = self._read_payload()
                status, response_payload = dispatch_api_request(api=api, method="DELETE", path=self.path, payload=payload)
                self._send_json(response_payload, status=HTTPStatus(status))
            except Exception as exc:
                self._send_error_json(exc)

        def _read_payload(self) -> dict[str, Any]:
            content_type = self.headers.get("Content-Type") or ""
            if content_type.startswith("multipart/form-data"):
                return self._read_multipart(content_type)
            return self._read_json()

        def _read_json(self) -> dict[str, Any]:
            length = int(self.headers.get("Content-Length") or 0)
            if length <= 0:
                return {}
            raw = self.rfile.read(length)
            try:
                payload = json.loads(raw.decode("utf-8"))
            except json.JSONDecodeError as exc:
                raise ValueError("request body must be valid JSON") from exc
            if not isinstance(payload, dict):
                raise ValueError("request body must be a JSON object")
            return payload

        def _read_stripe_webhook_payload(self) -> dict[str, Any]:
            length = int(self.headers.get("Content-Length") or 0)
            raw = self.rfile.read(length) if length > 0 else b""
            return {
                "raw_body": raw.decode("utf-8"),
                "stripe_signature": self.headers.get("Stripe-Signature") or "",
            }

        def _read_multipart(self, content_type: str) -> dict[str, Any]:
            boundary = _multipart_boundary(content_type)
            length = int(self.headers.get("Content-Length") or 0)
            raw = self.rfile.read(length)
            payload: dict[str, Any] = {}
            delimiter = b"--" + boundary
            for part in raw.split(delimiter):
                part = part.strip(b"\r\n")
                if not part or part == b"--":
                    continue
                if part.endswith(b"--"):
                    part = part[:-2].strip(b"\r\n")
                header_blob, _, body = part.partition(b"\r\n\r\n")
                headers = _parse_part_headers(header_blob)
                disposition = headers.get("content-disposition", "")
                name = _disposition_value(disposition, "name")
                if not name:
                    continue
                filename = _disposition_value(disposition, "filename")
                body = body.rstrip(b"\r\n")
                if filename:
                    payload["file"] = {
                        "filename": filename,
                        "content_type": headers.get("content-type", "application/octet-stream"),
                        "content": body,
                    }
                else:
                    payload[name] = body.decode("utf-8")
            return payload

        def _send_json(self, payload: dict[str, Any], *, status: HTTPStatus = HTTPStatus.OK) -> None:
            body = 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(body)))
            self.end_headers()
            self.wfile.write(body)

        def _send_error_json(self, exc: Exception) -> None:
            if isinstance(exc, (ValueError, StateTransitionError)):
                status = HTTPStatus.BAD_REQUEST
            elif isinstance(exc, (KeyError, FileNotFoundError)):
                status = HTTPStatus.NOT_FOUND
            elif isinstance(exc, PermissionError):
                status = HTTPStatus.FORBIDDEN
            else:
                status = HTTPStatus.INTERNAL_SERVER_ERROR
            self._send_json(
                {
                    "error": _safe_error_code(exc),
                    "message": _safe_error_message(exc),
                },
                status=status,
            )

    return LocalApiHandler


def dispatch_api_request(
    *,
    api: LocalPetApi,
    method: str,
    path: str,
    payload: dict[str, Any],
) -> tuple[int, dict[str, Any]]:
    try:
        return _dispatch_api_request(api=api, method=method.upper(), path=path, payload=payload)
    except Exception as exc:
        return _status_for_exception(exc), {
            "error": _safe_error_code(exc),
            "message": _safe_error_message(exc),
        }


def _dispatch_api_request(
    *,
    api: LocalPetApi,
    method: str,
    path: str,
    payload: dict[str, Any],
) -> tuple[int, dict[str, Any]]:
    parts = _path_parts(path)
    query = _query_params(path)
    if method == "GET":
        if parts == ["api", "health"]:
            return HTTPStatus.OK, api.health()
        if parts == ["api", "cloud", "me"]:
            if not hasattr(api, "get_cloud_me"):
                raise ValueError("cloud session API is not configured")
            return HTTPStatus.OK, api.get_cloud_me(
                session_id=str(payload.get("session_id") or query.get("session_id") or ""),
            )
        if len(parts) == 5 and parts[:3] == ["api", "cloud", "packages"] and parts[4] == "download-url":
            if not hasattr(api, "create_cloud_package_download_url"):
                raise ValueError("cloud package API is not configured")
            return HTTPStatus.OK, api.create_cloud_package_download_url(
                session_id=str(payload.get("session_id") or query.get("session_id") or ""),
                package_id=parts[3],
            )
        if len(parts) == 4 and parts[:3] == ["api", "cloud", "builds"]:
            if not hasattr(api, "get_cloud_build"):
                raise ValueError("cloud build API is not configured")
            return HTTPStatus.OK, api.get_cloud_build(
                session_id=str(payload.get("session_id") or query.get("session_id") or ""),
                build_id=parts[3],
            )
        if parts == ["api", "provider-metrics"]:
            return HTTPStatus.OK, api.get_provider_metrics(
                user_id=str(payload.get("user_id") or query.get("user_id") or "usr_local"),
                provider=_optional_text(query.get("provider")),
                build_id=_optional_text(query.get("build_id")),
            )
        if len(parts) == 3 and parts[:2] == ["api", "builds"]:
            return HTTPStatus.OK, api.get_build(parts[2])
        if len(parts) == 4 and parts[:2] == ["api", "builds"] and parts[3] == "preview":
            return HTTPStatus.OK, api.get_build_preview(parts[2])
        if len(parts) == 5 and parts[:2] == ["api", "builds"] and parts[3:] == ["preview", "contact-sheet"]:
            return HTTPStatus.OK, api.get_build_preview_asset(
                parts[2],
                user_id=str(payload.get("user_id") or query.get("user_id") or "usr_local"),
            )
        if len(parts) == 4 and parts[:2] == ["api", "packages"] and parts[3] == "download":
            return HTTPStatus.OK, api.get_package_download(
                parts[2],
                user_id=str(payload.get("user_id") or query.get("user_id") or "usr_local"),
            )
        if len(parts) == 4 and parts[:2] == ["api", "packages"] and parts[3] == "bundle":
            return HTTPStatus.OK, api.get_package_bundle(
                parts[2],
                user_id=str(payload.get("user_id") or query.get("user_id") or "usr_local"),
            )
        return HTTPStatus.NOT_FOUND, {"error": "not_found"}

    if method == "DELETE":
        if len(parts) == 3 and parts[:2] == ["api", "media"]:
            return HTTPStatus.OK, api.delete_media(
                parts[2],
                user_id=str(payload.get("user_id") or "usr_local"),
            )
        if len(parts) == 3 and parts[:2] == ["api", "packages"]:
            return HTTPStatus.OK, api.delete_package(
                parts[2],
                user_id=str(payload.get("user_id") or "usr_local"),
            )
        return HTTPStatus.NOT_FOUND, {"error": "not_found"}

    if method != "POST":
        return HTTPStatus.METHOD_NOT_ALLOWED, {"error": "method_not_allowed"}

    if parts == ["api", "cloud", "session"]:
        if not hasattr(api, "create_cloud_session"):
            raise ValueError("cloud session API is not configured")
        return HTTPStatus.CREATED, api.create_cloud_session(
            auth_uid=str(payload.get("auth_uid") or ""),
            email=_optional_text(payload.get("email")),
            device_id=str(payload.get("device_id") or ""),
            app_version=str(payload.get("app_version") or ""),
            channel=str(payload.get("channel") or "beta"),
        )
    if parts == ["api", "cloud", "invites", "redeem"]:
        if not hasattr(api, "redeem_cloud_invite"):
            raise ValueError("cloud invite API is not configured")
        return HTTPStatus.OK, api.redeem_cloud_invite(
            session_id=str(payload.get("session_id") or ""),
            invite_code=str(payload.get("invite_code") or ""),
        )
    if parts == ["api", "cloud", "checkout", "session"]:
        if not hasattr(api, "create_stripe_checkout_session"):
            raise ValueError("cloud checkout API is not configured")
        return HTTPStatus.CREATED, api.create_stripe_checkout_session(
            session_id=str(payload.get("session_id") or ""),
            client_surface=str(payload.get("client_surface") or "desktop_app"),
            success_url=str(payload.get("success_url") or ""),
            cancel_url=str(payload.get("cancel_url") or ""),
            amount_total_cents=int(payload.get("amount_total_cents") or 0),
        )
    if parts == ["api", "cloud", "webhooks", "stripe"]:
        if not hasattr(api, "handle_stripe_webhook"):
            raise ValueError("cloud stripe webhook API is not configured")
        return HTTPStatus.OK, api.handle_stripe_webhook(
            raw_body=str(payload.get("raw_body") or ""),
            stripe_signature=str(payload.get("stripe_signature") or ""),
        )
    if parts == ["api", "cloud", "pets"]:
        if not hasattr(api, "create_cloud_pet_draft"):
            raise ValueError("cloud pet API is not configured")
        return HTTPStatus.CREATED, api.create_cloud_pet_draft(
            auth_uid=str(payload.get("auth_uid") or ""),
            email=_optional_text(payload.get("email")),
            device_id=str(payload.get("device_id") or ""),
            app_version=str(payload.get("app_version") or ""),
            channel=str(payload.get("channel") or "beta"),
            pet_name=str(payload.get("pet_name") or ""),
            species=_optional_text(payload.get("species")),
            breed=_optional_text(payload.get("breed")),
            personality_notes=_optional_text(payload.get("personality_notes")),
            typical_gestures=_optional_text(payload.get("typical_gestures")),
            memory_notes=_optional_text(payload.get("memory_notes")),
        )
    if len(parts) == 6 and parts[:3] == ["api", "cloud", "pets"] and parts[4:] == ["media", "uploads"]:
        if not hasattr(api, "create_cloud_media_upload"):
            raise ValueError("cloud media API is not configured")
        return HTTPStatus.CREATED, api.create_cloud_media_upload(
            user_id=str(payload.get("user_id") or ""),
            pet_id=parts[3],
            media_type=str(payload.get("media_type") or "photo"),
            mime_type=str(payload.get("mime_type") or ""),
            size_bytes=int(payload.get("size_bytes") or 0),
            sha256=str(payload.get("sha256") or ""),
            original_filename=_optional_text(payload.get("original_filename")),
        )
    if len(parts) == 7 and parts[:3] == ["api", "cloud", "pets"] and parts[4] == "media" and parts[6] == "complete":
        if not hasattr(api, "complete_cloud_media_upload"):
            raise ValueError("cloud media API is not configured")
        return HTTPStatus.OK, api.complete_cloud_media_upload(
            user_id=str(payload.get("user_id") or ""),
            pet_id=parts[3],
            media_id=parts[5],
        )
    if len(parts) == 5 and parts[:3] == ["api", "cloud", "pets"] and parts[4] == "builds":
        if not hasattr(api, "create_cloud_build"):
            raise ValueError("cloud build API is not configured")
        return HTTPStatus.CREATED, api.create_cloud_build(
            session_id=str(payload.get("session_id") or ""),
            pet_id=parts[3],
            media_ids=list(payload.get("media_ids") or []),
            image_provider=str(payload.get("image_provider") or "openai"),
            image_model=str(payload.get("image_model") or ""),
        )
    if len(parts) == 5 and parts[:3] == ["api", "cloud", "builds"] and parts[4] == "approve":
        if not hasattr(api, "approve_cloud_build"):
            raise ValueError("cloud build API is not configured")
        return HTTPStatus.OK, api.approve_cloud_build(
            session_id=str(payload.get("session_id") or ""),
            build_id=parts[3],
            approval_answers={
                "looks_like_pet": payload.get("looks_like_pet") is True,
                "feels_comfortable": payload.get("feels_comfortable") is True,
                "actions_fit_long_term": payload.get("actions_fit_long_term") is True,
                "written_feedback": _optional_text(payload.get("written_feedback")),
            },
        )
    if len(parts) == 5 and parts[:3] == ["api", "cloud", "media"] and parts[4] == "delete":
        if not hasattr(api, "delete_cloud_media"):
            raise ValueError("cloud media API is not configured")
        return HTTPStatus.OK, api.delete_cloud_media(
            session_id=str(payload.get("session_id") or ""),
            media_id=parts[3],
        )
    if len(parts) == 5 and parts[:3] == ["api", "cloud", "packages"] and parts[4] == "delete":
        if not hasattr(api, "delete_cloud_package"):
            raise ValueError("cloud package API is not configured")
        return HTTPStatus.OK, api.delete_cloud_package(
            session_id=str(payload.get("session_id") or ""),
            package_id=parts[3],
        )
    if len(parts) == 5 and parts[:3] == ["api", "cloud", "packages"] and parts[4] == "runtime-check":
        if not hasattr(api, "check_cloud_package_runtime"):
            raise ValueError("cloud package API is not configured")
        return HTTPStatus.OK, api.check_cloud_package_runtime(
            session_id=str(payload.get("session_id") or ""),
            package_id=parts[3],
        )
    if parts == ["api", "pets"]:
        return HTTPStatus.CREATED, api.create_pet(
            user_id=str(payload.get("user_id") or "usr_local"),
            pet_name=str(payload.get("pet_name") or ""),
            species=_optional_text(payload.get("species")),
            breed=_optional_text(payload.get("breed")),
            personality_notes=_optional_text(payload.get("personality_notes")),
            typical_gestures=_optional_text(payload.get("typical_gestures")),
            memory_notes=_optional_text(payload.get("memory_notes")),
        )
    if parts == ["api", "checkout", "test"]:
        return HTTPStatus.CREATED, api.create_test_checkout(
            user_id=str(payload.get("user_id") or "usr_local"),
            build_id=str(payload.get("build_id") or ""),
            product_code=str(payload.get("product_code") or "custom_pet_build"),
        )
    if parts == ["api", "subscriptions", "test"]:
        return HTTPStatus.CREATED, api.grant_test_subscription(
            user_id=str(payload.get("user_id") or "usr_local"),
            pet_id=_optional_text(payload.get("pet_id")),
        )
    if len(parts) == 5 and parts[:2] == ["api", "pets"] and parts[3:] == ["media", "upload"]:
        file_payload = payload.get("file")
        if not isinstance(file_payload, dict):
            raise ValueError("image file is required")
        return HTTPStatus.CREATED, api.upload_media_bytes(
            user_id=str(payload.get("user_id") or "usr_local"),
            pet_id=parts[2],
            filename=str(file_payload.get("filename") or "upload"),
            content_type=str(file_payload.get("content_type") or "application/octet-stream"),
            content=bytes(file_payload.get("content") or b""),
        )
    if len(parts) == 4 and parts[:2] == ["api", "pets"] and parts[3] == "media":
        return HTTPStatus.CREATED, api.create_media_record(
            user_id=str(payload.get("user_id") or "usr_local"),
            pet_id=parts[2],
            media_type=str(payload.get("media_type") or "photo"),
            mime_type=str(payload.get("mime_type") or ""),
            original_filename=_optional_text(payload.get("original_filename")),
            storage_ref=str(payload.get("storage_ref") or f"local-private://pet-media/{parts[2]}"),
        )
    if len(parts) == 4 and parts[:2] == ["api", "pets"] and parts[3] == "consent":
        return HTTPStatus.OK, api.grant_consent(
            user_id=str(payload.get("user_id") or "usr_local"),
            pet_id=parts[2],
            boundary_acknowledged=payload.get("boundary_acknowledged") is True,
            consent_version=str(payload.get("consent_version") or "p0-local"),
            external_processor_name=_optional_text(payload.get("external_processor_name")),
            external_processor_consent=payload.get("external_processor_consent") is True,
            external_processor_consent_version=_optional_text(payload.get("external_processor_consent_version")),
        )
    if len(parts) == 4 and parts[:2] == ["api", "pets"] and parts[3] == "builds":
        return HTTPStatus.CREATED, api.create_build(
            user_id=str(payload.get("user_id") or "usr_local"),
            pet_id=parts[2],
            media_ids=[str(item) for item in payload.get("media_ids", [])],
            image_provider=str(payload.get("image_provider") or "apimart"),
            image_model=str(payload.get("image_model") or "gpt-image-2"),
        )
    if len(parts) == 4 and parts[:2] == ["api", "media"] and parts[3] == "quality":
        return HTTPStatus.OK, api.mark_media_quality(
            parts[2],
            source_quality=str(payload.get("source_quality") or "passed"),
        )
    if len(parts) == 4 and parts[:2] == ["api", "builds"] and parts[3] == "process":
        return HTTPStatus.OK, api.process_build(parts[2])
    if len(parts) == 4 and parts[:2] == ["api", "builds"] and parts[3] == "approve":
        return HTTPStatus.OK, api.approve_build(
            parts[2],
            user_id=str(payload.get("user_id") or "usr_local"),
            approval_answers=_approval_answers_from_payload(payload),
        )
    if len(parts) == 4 and parts[:2] == ["api", "builds"] and parts[3] == "download":
        return HTTPStatus.OK, api.create_package_download(
            parts[2],
            user_id=str(payload.get("user_id") or "usr_local"),
            entitlement_status=_optional_text(payload.get("entitlement_status")),
        )
    return HTTPStatus.NOT_FOUND, {"error": "not_found"}


def _path_parts(path: str) -> list[str]:
    clean = urlsplit(path).path.strip("/")
    if not clean:
        return []
    return [unquote(part) for part in clean.split("/") if part]


def _query_params(path: str) -> dict[str, str]:
    parsed = parse_qs(urlsplit(path).query)
    return {key: values[0] for key, values in parsed.items() if values}


def _multipart_boundary(content_type: str) -> bytes:
    for segment in content_type.split(";"):
        segment = segment.strip()
        if segment.startswith("boundary="):
            value = segment.removeprefix("boundary=").strip('"')
            if value:
                return value.encode("utf-8")
    raise ValueError("multipart boundary is required")


def _parse_part_headers(blob: bytes) -> dict[str, str]:
    headers: dict[str, str] = {}
    for line in blob.decode("utf-8", errors="replace").split("\r\n"):
        name, _, value = line.partition(":")
        if name and value:
            headers[name.strip().lower()] = value.strip()
    return headers


def _disposition_value(disposition: str, key: str) -> str:
    prefix = f'{key}="'
    for segment in disposition.split(";"):
        segment = segment.strip()
        if segment.startswith(prefix) and segment.endswith('"'):
            return segment[len(prefix) : -1]
    return ""


def _optional_text(value: Any) -> str | None:
    if value is None:
        return None
    text = str(value).strip()
    return text or None


def _approval_answers_from_payload(payload: dict[str, Any]) -> dict[str, Any]:
    nested = payload.get("approval_answers")
    if isinstance(nested, dict):
        return dict(nested)
    return {
        "looks_like_pet": payload.get("looks_like_pet") is True,
        "feels_comfortable": payload.get("feels_comfortable") is True,
        "actions_fit_long_term": payload.get("actions_fit_long_term") is True,
    }


def _safe_error_code(exc: Exception) -> str:
    if isinstance(exc, StateTransitionError):
        return "invalid_state_transition"
    if isinstance(exc, PermissionError):
        return "forbidden"
    if isinstance(exc, KeyError):
        return "not_found"
    if isinstance(exc, ValueError):
        return "invalid_request"
    return "internal_error"


def _status_for_exception(exc: Exception) -> int:
    if isinstance(exc, (ValueError, StateTransitionError)):
        return int(HTTPStatus.BAD_REQUEST)
    if isinstance(exc, PermissionError):
        return int(HTTPStatus.FORBIDDEN)
    if isinstance(exc, (KeyError, FileNotFoundError)):
        return int(HTTPStatus.NOT_FOUND)
    return int(HTTPStatus.INTERNAL_SERVER_ERROR)


def _safe_error_message(exc: Exception) -> str:
    if isinstance(exc, (StateTransitionError, ValueError)):
        return str(exc)
    if isinstance(exc, PermissionError):
        return "not allowed"
    if isinstance(exc, KeyError):
        return "resource not found"
    return "request failed"
