from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

from scienith_openapi_contract_tooling.openapi.bundle import bundle_openapi_spec_for_docs
from scienith_openapi_contract_tooling.openapi.load import load_openapi_spec


HTTP_METHODS = ("get", "post", "put", "patch", "delete", "head", "options")


def _ts_string(s: str) -> str:
    return json.dumps(s, ensure_ascii=False)


def _json_pointer_get(doc: Any, pointer: str) -> Any:
    if pointer.startswith("#"):
        pointer = pointer[1:]
    if pointer.startswith("/"):
        pointer = pointer[1:]
    if pointer == "":
        return doc
    cur: Any = doc
    for raw in pointer.split("/"):
        token = raw.replace("~1", "/").replace("~0", "~")
        if isinstance(cur, list):
            cur = cur[int(token)]
        elif isinstance(cur, dict):
            cur = cur[token]
        else:
            raise KeyError(f"JSON Pointer 不可达：{raw}")
    return cur


def _ref_resolve(spec: dict[str, Any], ref: str) -> Any:
    if not ref.startswith("#"):
        raise ValueError(f"仅支持内部 $ref：{ref}")
    return _json_pointer_get(spec, ref)


def _msw_path_from_openapi(path: str) -> str:
    # OpenAPI: /foo/{id} -> MSW: /foo/:id
    return re.sub(r"\{([A-Za-z0-9_]+)\}", r":\1", path)


def _pick_response(operation: dict[str, Any]) -> tuple[int, dict[str, Any]] | None:
    responses = operation.get("responses") or {}
    if not isinstance(responses, dict) or not responses:
        return None

    candidates: list[tuple[int, dict[str, Any]]] = []
    for code_str, resp in responses.items():
        if not isinstance(resp, dict):
            continue
        if code_str == "default":
            continue
        if not str(code_str).isdigit():
            continue
        code = int(code_str)
        if 200 <= code <= 299:
            candidates.append((code, resp))

    if not candidates:
        return None

    # prefer common success codes
    for preferred in (200, 201, 204):
        for code, resp in candidates:
            if code == preferred:
                return code, resp

    candidates.sort(key=lambda x: x[0])
    return candidates[0]


def _pick_json_schema(resp: dict[str, Any]) -> Any | None:
    content = resp.get("content") or {}
    if not isinstance(content, dict):
        return None
    app_json = content.get("application/json")
    if isinstance(app_json, dict):
        schema = app_json.get("schema")
        if schema is not None:
            return schema

    # fallback to any +json
    for ctype, entry in content.items():
        if not isinstance(ctype, str) or not ctype.endswith("+json"):
            continue
        if not isinstance(entry, dict):
            continue
        schema = entry.get("schema")
        if schema is not None:
            return schema
    return None


def _sample_from_schema(
    spec: dict[str, Any],
    schema_any: Any,
    *,
    depth: int = 0,
    visiting: set[str] | None = None,
) -> Any:
    if visiting is None:
        visiting = set()
    if depth > 6:
        return None

    if schema_any is True:
        return {}
    if schema_any is False:
        return None
    if not isinstance(schema_any, dict):
        return None

    if "example" in schema_any:
        return schema_any["example"]
    if "default" in schema_any:
        return schema_any["default"]
    if "const" in schema_any:
        return schema_any["const"]
    enum = schema_any.get("enum")
    if isinstance(enum, list) and enum:
        return enum[0]

    ref = schema_any.get("$ref")
    if isinstance(ref, str):
        if ref in visiting:
            return None
        visiting.add(ref)
        try:
            resolved = _ref_resolve(spec, ref)
            return _sample_from_schema(spec, resolved, depth=depth + 1, visiting=visiting)
        finally:
            visiting.remove(ref)

    for key in ("oneOf", "anyOf"):
        arr = schema_any.get(key)
        if isinstance(arr, list) and arr:
            return _sample_from_schema(spec, arr[0], depth=depth + 1, visiting=visiting)

    all_of = schema_any.get("allOf")
    if isinstance(all_of, list) and all_of:
        merged: dict[str, Any] = {}
        for s in all_of:
            v = _sample_from_schema(spec, s, depth=depth + 1, visiting=visiting)
            if isinstance(v, dict):
                merged.update(v)
        if merged:
            return merged
        return _sample_from_schema(spec, all_of[0], depth=depth + 1, visiting=visiting)

    t = schema_any.get("type")
    if isinstance(t, list) and t:
        # pick first non-null
        for tt in t:
            if tt == "null":
                continue
            return _sample_from_schema(spec, {**schema_any, "type": tt}, depth=depth + 1, visiting=visiting)
        return None

    if t == "string" or t is None:
        fmt = schema_any.get("format")
        if fmt == "uuid":
            return "00000000-0000-0000-0000-000000000000"
        if fmt == "email":
            return "e2e@example.com"
        if fmt == "date-time":
            return "1970-01-01T00:00:00Z"
        if fmt == "date":
            return "1970-01-01"
        min_len = schema_any.get("minLength")
        if isinstance(min_len, int) and min_len > 0:
            return "x" * min(min_len, 8)
        return "string"

    if t == "integer" or t == "number":
        return 0

    if t == "boolean":
        return False

    if t == "array":
        items = schema_any.get("items") or {}
        return [_sample_from_schema(spec, items, depth=depth + 1, visiting=visiting)]

    if t == "object" or "properties" in schema_any or "additionalProperties" in schema_any:
        props = schema_any.get("properties") or {}
        required = schema_any.get("required") or []
        if not isinstance(props, dict):
            props = {}
        if not isinstance(required, list):
            required = []

        out: dict[str, Any] = {}
        for name in required:
            if name in props:
                out[str(name)] = _sample_from_schema(spec, props[name], depth=depth + 1, visiting=visiting)

        # 如果没有 required，尽量给一个“最小但可用”的字段
        if not out and props:
            first_key = sorted(props.keys())[0]
            out[str(first_key)] = _sample_from_schema(spec, props[first_key], depth=depth + 1, visiting=visiting)
        return out

    return None


def openapi_to_msw_handlers_ts(*, root_path: str, out_path: str, bundle: bool = True) -> None:
    root = Path(root_path).resolve()
    out = Path(out_path)

    if bundle:
        spec = bundle_openapi_spec_for_docs(root_path=root)
    else:
        spec = load_openapi_spec(root)

    paths = spec.get("paths") or {}
    if not isinstance(paths, dict) or not paths:
        raise ValueError("未找到 paths。")

    lines: list[str] = []
    lines.append("// This file is GENERATED. Do not edit by hand.")
    lines.append("// Source: OpenAPI paths")
    lines.append("")
    lines.append('import { http, HttpResponse } from "msw";')
    lines.append("")
    lines.append("export const generatedHandlers = [")

    emitted = 0
    for path, item in sorted(paths.items(), key=lambda x: str(x[0])):
        if not isinstance(item, dict):
            continue
        for method in HTTP_METHODS:
            op = item.get(method)
            if not isinstance(op, dict):
                continue

            picked = _pick_response(op)
            status = picked[0] if picked else 200
            resp = picked[1] if picked else {}
            schema = _pick_json_schema(resp) if isinstance(resp, dict) else None
            sample = _sample_from_schema(spec, schema, depth=0) if schema is not None else {}
            # 尽量让返回是 JSON（即便 schema 不存在），以减少前端解析崩溃概率
            msw_path = _msw_path_from_openapi(str(path))
            lines.append(f"  // {method.upper()} {path}")
            lines.append(f"  http.{method}({_ts_string(msw_path)}, () => {{")
            lines.append("    // TODO: implement mock behavior if needed (stateful create/list/etc.)")
            lines.append(
                f"    return HttpResponse.json({json.dumps(sample, ensure_ascii=False)}, {{ status: {status} }});"
            )
            lines.append("  }),")
            emitted += 1

    lines.append("] as const;")
    lines.append("")

    if emitted == 0:
        raise ValueError("未从 OpenAPI paths 生成任何 handler（请检查 paths 结构）。")

    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines), encoding="utf-8")
