from __future__ import annotations

from pathlib import Path
from typing import Any

from scienith_openapi_contract_tooling.openapi.load import load_openapi_spec
from scienith_openapi_contract_tooling.openapi.resolve import normalize_schema_refs, resolve_components_schemas

_SOURCE_BASE_DIR_KEY = "__bundle_source_base_dir__"


def _split_ref(ref: str) -> tuple[str, str]:
    if "#" in ref:
        path_part, frag = ref.split("#", 1)
        return path_part, frag
    return ref, ""


def _json_pointer_get(doc: Any, pointer: str) -> Any:
    if pointer.startswith("/"):
        pointer = pointer[1:]
    if pointer == "":
        return doc

    cur: Any = doc
    for raw_token in pointer.split("/"):
        token = raw_token.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_token}")
    return cur


def _canonical_ref(base_dir: Path, ref: str) -> str:
    path_part, frag = _split_ref(ref)
    if path_part == "":
        return f"#{frag}"
    p = (base_dir / path_part).resolve()
    return f"{p}#{frag}"


def _load_external_ref(*, base_dir: Path, ref: str) -> tuple[Any, Path]:
    path_part, frag = _split_ref(ref)
    if path_part == "":
        raise ValueError("内部 $ref 不应通过 _load_external_ref 加载。")

    target_path = (base_dir / path_part).resolve()
    doc = load_openapi_spec(target_path)

    if frag.startswith("/"):
        value = _json_pointer_get(doc, frag)
    elif frag == "":
        value = doc
    else:
        raise ValueError(f"不支持的 $ref fragment：#{frag}")

    if isinstance(value, dict):
        value = {_SOURCE_BASE_DIR_KEY: str(target_path.parent), **value}

    return value, target_path.parent


def bundle_openapi_spec_for_docs(*, root_path: Path) -> dict[str, Any]:
    """
    将 OpenAPI 根文件及其引用的外部文件 $ref 尽可能 bundle 为单文件对象，便于 Redoc/Swagger 等文档工具使用。

    策略：
    - 优先把 components/schemas 的文件 $ref 归一化为字典（并把其子树内的文件 $ref 改写为内部 ref）。
    - 文档渲染时若遇到指向这些 schema 文件的 $ref，则改写为 `#/components/schemas/<Name>`。
    - 其他文件 $ref 则直接内联展开。
    """
    root_path = root_path.resolve()
    root_dir = root_path.parent
    spec = load_openapi_spec(root_path)
    if not isinstance(spec, dict):
        raise ValueError("OpenAPI 根文件解析结果不是对象。")

    # components/schemas 可能不存在（例如仅定义了 paths 或把 schema 放在其他位置）。
    # 对于文档 bundle：没有 schemas 也应当可用（至少应能内联展开其他外部 $ref）。
    try:
        resolved = resolve_components_schemas(root_path=root_path)
    except ValueError as exc:
        if "未找到 components/schemas" in str(exc):
            resolved = {}
        else:
            raise

    if resolved:
        normalized_schemas = normalize_schema_refs(resolved=resolved, root_dir=root_dir)
        spec.setdefault("components", {})
        spec["components"]["schemas"] = {
            name: {_SOURCE_BASE_DIR_KEY: str(resolved[name].base_dir), **schema}
            for name, schema in normalized_schemas.items()
        }

    file_ref_to_name: dict[str, str] = {}
    for name, rs in resolved.items():
        if rs.source_ref:
            file_ref_to_name[rs.source_ref] = name

    visiting: set[str] = set()

    def bundle(node: Any, *, base_dir: Path) -> Any:
        if isinstance(node, dict):
            source_base_dir = node.get(_SOURCE_BASE_DIR_KEY)
            if isinstance(source_base_dir, str):
                base_dir = Path(source_base_dir)

            ref = node.get("$ref")
            if isinstance(ref, str):
                path_part, _ = _split_ref(ref)
                if path_part != "":
                    canonical = _canonical_ref(base_dir, ref)
                    mapped = file_ref_to_name.get(canonical)
                    if mapped:
                        if len(node) == 1:
                            return {"$ref": f"#/components/schemas/{mapped}"}
                        copied = {
                            k: bundle(v, base_dir=base_dir)
                            for k, v in node.items()
                            if k not in {"$ref", _SOURCE_BASE_DIR_KEY}
                        }
                        copied["$ref"] = f"#/components/schemas/{mapped}"
                        return copied

                    if canonical in visiting:
                        raise ValueError(f"检测到循环外部 $ref：{canonical}")
                    visiting.add(canonical)
                    try:
                        target, next_base = _load_external_ref(base_dir=base_dir, ref=ref)
                        bundled_target = bundle(target, base_dir=next_base)
                    finally:
                        visiting.remove(canonical)

                    if len(node) == 1:
                        return bundled_target

                    extras = {
                        k: bundle(v, base_dir=base_dir)
                        for k, v in node.items()
                        if k not in {"$ref", _SOURCE_BASE_DIR_KEY}
                    }
                    return {"allOf": [bundled_target], **extras}

            return {
                k: bundle(v, base_dir=base_dir)
                for k, v in node.items()
                if k != _SOURCE_BASE_DIR_KEY
            }

        if isinstance(node, list):
            return [bundle(v, base_dir=base_dir) for v in node]

        return node

    return bundle(spec, base_dir=root_dir)
