from __future__ import annotations

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

import yaml


def build_skills_management_artifacts(
    *,
    openapi_path: str | Path,
    openapi_bundle_out: str | Path,
    contract_summary_out: str | Path,
    operation_maps_ts_out: str | Path,
    python_validator_out: str | Path,
    python_validator_init_out: str | Path | None = None,
) -> None:
    openapi_path = Path(openapi_path)
    doc = yaml.safe_load(openapi_path.read_text(encoding="utf-8"))
    operation_ids: list[str] = []
    refs: list[str] = []
    _walk(doc, operation_ids, refs)
    if len(operation_ids) != len(set(operation_ids)):
        raise SystemExit("duplicate operationId in contracts/openapi.yaml")
    for ref in refs:
        _resolve_pointer(doc, ref)
    operations = _collect_operations(doc)

    summary = {
        "openapi": doc["openapi"],
        "title": doc["info"]["title"],
        "version": doc["info"]["version"],
        "path_count": len(doc["paths"]),
        "operation_ids": sorted(operation_ids),
        "schema_count": len(doc.get("components", {}).get("schemas", {})),
        "schemas": sorted(doc.get("components", {}).get("schemas", {})),
    }
    openapi_bundle_out = Path(openapi_bundle_out)
    contract_summary_out = Path(contract_summary_out)
    operation_maps_ts_out = Path(operation_maps_ts_out)
    python_validator_out = Path(python_validator_out)

    openapi_bundle_out.parent.mkdir(parents=True, exist_ok=True)
    contract_summary_out.parent.mkdir(parents=True, exist_ok=True)
    operation_maps_ts_out.parent.mkdir(parents=True, exist_ok=True)
    python_validator_out.parent.mkdir(parents=True, exist_ok=True)

    openapi_bundle_out.write_text(
        json.dumps(doc, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    contract_summary_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    operation_maps_ts_out.write_text(_render_ts_types(doc, operations), encoding="utf-8")

    if python_validator_init_out is not None:
        init_out = Path(python_validator_init_out)
        init_out.parent.mkdir(parents=True, exist_ok=True)
        init_out.write_text('"""Generated OpenAPI contract artifacts."""\n', encoding="utf-8")
    python_validator_out.write_text(
        _render_python_contract(doc, operations),
        encoding="utf-8",
    )

    print(
        "contract-artifacts-ok "
        f"operations={len(operation_ids)} schemas={summary['schema_count']} "
        f"outputs={openapi_bundle_out},{operation_maps_ts_out},{python_validator_out}"
    )


def _walk(value, operation_ids: list[str], refs: list[str]) -> None:
    if isinstance(value, dict):
        if "$ref" in value:
            refs.append(value["$ref"])
        for key, child in value.items():
            if key == "operationId":
                operation_ids.append(str(child))
            _walk(child, operation_ids, refs)
    elif isinstance(value, list):
        for child in value:
            _walk(child, operation_ids, refs)


def _resolve_pointer(root, pointer: str):
    if not pointer.startswith("#/"):
        raise SystemExit(f"unsupported ref: {pointer}")
    current = root
    for raw_part in pointer[2:].split("/"):
        part = raw_part.replace("~1", "/").replace("~0", "~")
        if not isinstance(current, dict) or part not in current:
            raise SystemExit(f"missing ref target: {pointer}")
        current = current[part]
    return current


def _collect_operations(doc: dict[str, Any]) -> dict[str, dict[str, Any]]:
    operations: dict[str, dict[str, Any]] = {}
    for path, path_item in doc.get("paths", {}).items():
        if not isinstance(path_item, dict):
            continue
        for method, operation in path_item.items():
            if method.lower() not in {"get", "put", "post", "delete", "patch"}:
                continue
            if not isinstance(operation, dict):
                continue
            operation_id = str(operation.get("operationId") or "")
            if not operation_id:
                raise SystemExit(f"missing operationId for {method.upper()} {path}")
            request_schema = _json_request_schema(operation)
            response_schema = _json_response_schema(doc, operation)
            if response_schema is None:
                raise SystemExit(f"missing JSON success response schema for operationId={operation_id}")
            operations[operation_id] = {
                "method": method.upper(),
                "path": path,
                "request_schema": request_schema,
                "response_schema": response_schema,
            }
    return operations


def _json_request_schema(operation: dict[str, Any]) -> dict[str, Any] | None:
    request_body = operation.get("requestBody")
    if not isinstance(request_body, dict):
        return None
    content = request_body.get("content")
    if not isinstance(content, dict):
        return None
    json_content = content.get("application/json")
    if not isinstance(json_content, dict):
        return None
    schema = json_content.get("schema")
    if not isinstance(schema, dict):
        raise SystemExit(f"invalid JSON request schema for operationId={operation.get('operationId')}")
    return schema


def _json_response_schema(doc: dict[str, Any], operation: dict[str, Any]) -> dict[str, Any] | None:
    responses = operation.get("responses")
    if not isinstance(responses, dict):
        return None
    for status in ("200", "201", "202", "204"):
        response = responses.get(status)
        if response is None:
            continue
        if isinstance(response, dict) and "$ref" in response:
            response = _resolve_pointer(doc, response["$ref"])
        if not isinstance(response, dict):
            continue
        content = response.get("content")
        if not isinstance(content, dict):
            return {"type": "null"} if status == "204" else None
        json_content = content.get("application/json")
        if not isinstance(json_content, dict):
            continue
        schema = json_content.get("schema")
        if not isinstance(schema, dict):
            raise SystemExit(f"invalid JSON response schema for operationId={operation.get('operationId')}")
        return schema
    return None


def _render_ts_types(doc: dict[str, Any], operations: dict[str, dict[str, Any]]) -> str:
    schemas = doc.get("components", {}).get("schemas", {})
    schema_lines = [
        "// Generated by scripts/build_contract_artifacts.py. Do not edit by hand.",
        "",
    ]
    for name, schema in schemas.items():
        schema_lines.append(f"export type {name} = {_ts_type(schema, root=doc)};")
        schema_lines.append("")
    schema_map = [f"    {json.dumps(name)}: {name};" for name in sorted(schemas)]
    request_map = []
    response_map = []
    operation_map = []
    for operation_id, contract in sorted(operations.items()):
        request_type = _ts_type(contract["request_schema"], root=doc) if contract["request_schema"] else "undefined"
        response_type = _ts_type(contract["response_schema"], root=doc)
        request_map.append(f"  {json.dumps(operation_id)}: {request_type};")
        response_map.append(f"  {json.dumps(operation_id)}: {response_type};")
        operation_map.append(
            "  "
            + json.dumps(operation_id)
            + f": {{ method: {json.dumps(contract['method'])}; path: {json.dumps(contract['path'])}; "
            + f"request: OperationRequestMap[{json.dumps(operation_id)}]; "
            + f"response: OperationResponseMap[{json.dumps(operation_id)}]; }};"
        )
    schema_lines.extend(
        [
            "export type ComponentSchemaMap = {",
            *schema_map,
            "};",
            "",
            "export type OperationRequestMap = {",
            *request_map,
            "};",
            "",
            "export type OperationResponseMap = {",
            *response_map,
            "};",
            "",
            "export type OperationMap = {",
            *operation_map,
            "};",
            "",
        ]
    )
    return "\n".join(schema_lines)


def _ts_type(schema: dict[str, Any] | None, *, root: dict[str, Any]) -> str:
    if schema is None:
        return "unknown"
    if "$ref" in schema:
        return _ref_name(str(schema["$ref"]))
    if "allOf" in schema:
        return " & ".join(f"({_ts_type(item, root=root)})" for item in schema["allOf"])
    if "oneOf" in schema:
        return " | ".join(f"({_ts_type(item, root=root)})" for item in schema["oneOf"])
    if "anyOf" in schema:
        return " | ".join(f"({_ts_type(item, root=root)})" for item in schema["anyOf"])
    if "const" in schema:
        return json.dumps(schema["const"])
    if "enum" in schema:
        return " | ".join(json.dumps(item) for item in schema["enum"])
    schema_type = schema.get("type")
    if isinstance(schema_type, list):
        return " | ".join(_ts_type({**schema, "type": item}, root=root) for item in schema_type)
    if schema_type == "string":
        return "string"
    if schema_type in {"integer", "number"}:
        return "number"
    if schema_type == "boolean":
        return "boolean"
    if schema_type == "null":
        return "null"
    if schema_type == "array":
        return f"Array<{_ts_type(schema.get('items') or {}, root=root)}>"
    if schema_type == "object" or "properties" in schema or "additionalProperties" in schema:
        return _ts_object_type(schema, root=root)
    return "unknown"


def _ts_object_type(schema: dict[str, Any], *, root: dict[str, Any]) -> str:
    properties = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
    required = set(schema.get("required") or [])
    parts: list[str] = []
    for name, child in properties.items():
        optional = "" if name in required else "?"
        parts.append(f"{json.dumps(name)}{optional}: {_ts_type(child, root=root)};")
    additional = schema.get("additionalProperties")
    if additional is True:
        parts.append("[key: string]: unknown;")
    elif isinstance(additional, dict):
        parts.append(f"[key: string]: {_ts_type(additional, root=root)};")
    if not parts:
        return "Record<string, unknown>"
    return "{ " + " ".join(parts) + " }"


def _ref_name(ref: str) -> str:
    prefix = "#/components/schemas/"
    if not ref.startswith(prefix):
        raise SystemExit(f"unsupported TypeScript ref target: {ref}")
    return ref.removeprefix(prefix)


def _render_python_contract(doc: dict[str, Any], operations: dict[str, dict[str, Any]]) -> str:
    payload = {
        "openapi": doc,
        "schemas": doc.get("components", {}).get("schemas", {}),
        "operations": operations,
    }
    return (
        '"""Generated OpenAPI contract validators. Do not edit by hand."""\n\n'
        "from __future__ import annotations\n\n"
        "import json\n"
        "from collections.abc import Mapping, Sequence\n"
        "from typing import Any\n\n"
        "CONTRACT: dict[str, Any] = json.loads(\n"
        + repr(json.dumps(payload, ensure_ascii=False, indent=2))
        + "\n)\n\n"
        + textwrap.dedent(
            '''
            COMPONENT_SCHEMAS: dict[str, Any] = CONTRACT["schemas"]
            OPERATION_CONTRACTS: dict[str, Any] = CONTRACT["operations"]


            class ContractValidationError(ValueError):
                """Raised when a payload does not match the generated OpenAPI contract."""


            def validate_request(operation_id: str, payload: Any) -> Any:
                operation = _operation(operation_id)
                schema = operation.get("request_schema")
                if schema is None:
                    if payload in (None, {}):
                        return payload
                    raise ContractValidationError(f"{operation_id} does not accept a JSON request body")
                _validate_schema(schema, payload, path=f"{operation_id}.request")
                return payload


            def validate_response(operation_id: str, payload: Any) -> Any:
                operation = _operation(operation_id)
                schema = operation["response_schema"]
                _validate_schema(schema, payload, path=f"{operation_id}.response")
                return payload


            def _operation(operation_id: str) -> Mapping[str, Any]:
                try:
                    return OPERATION_CONTRACTS[operation_id]
                except KeyError as exc:
                    raise ContractValidationError(f"unknown operationId: {operation_id}") from exc


            def _validate_schema(schema: Mapping[str, Any], value: Any, *, path: str) -> None:
                if "$ref" in schema:
                    _validate_schema(_resolve_ref(schema["$ref"]), value, path=path)
                    return
                for item in schema.get("allOf", []) or []:
                    _validate_schema(item, value, path=path)
                if "anyOf" in schema:
                    _validate_any(schema["anyOf"], value, path=path)
                    return
                if "oneOf" in schema:
                    _validate_one(schema["oneOf"], value, path=path)
                    return
                if "const" in schema and value != schema["const"]:
                    raise ContractValidationError(f"{path} must equal {schema['const']!r}")
                if "enum" in schema and value not in schema["enum"]:
                    raise ContractValidationError(f"{path} must be one of {schema['enum']!r}")
                schema_type = schema.get("type")
                if isinstance(schema_type, list):
                    errors = []
                    for item_type in schema_type:
                        try:
                            _validate_schema({**schema, "type": item_type}, value, path=path)
                            return
                        except ContractValidationError as exc:
                            errors.append(str(exc))
                    raise ContractValidationError(f"{path} does not match any allowed type: {errors}")
                if schema_type == "null":
                    if value is not None:
                        raise ContractValidationError(f"{path} must be null")
                    return
                if value is None:
                    raise ContractValidationError(f"{path} must not be null")
                if schema_type == "string":
                    if not isinstance(value, str):
                        raise ContractValidationError(f"{path} must be a string")
                    return
                if schema_type == "integer":
                    if not isinstance(value, int) or isinstance(value, bool):
                        raise ContractValidationError(f"{path} must be an integer")
                    return
                if schema_type == "number":
                    if not isinstance(value, int | float) or isinstance(value, bool):
                        raise ContractValidationError(f"{path} must be a number")
                    return
                if schema_type == "boolean":
                    if not isinstance(value, bool):
                        raise ContractValidationError(f"{path} must be a boolean")
                    return
                if schema_type == "array":
                    if not _is_sequence(value):
                        raise ContractValidationError(f"{path} must be an array")
                    item_schema = schema.get("items")
                    if isinstance(item_schema, Mapping):
                        for index, item in enumerate(value):
                            _validate_schema(item_schema, item, path=f"{path}[{index}]")
                    return
                if schema_type == "object" or "properties" in schema or "additionalProperties" in schema:
                    _validate_object(schema, value, path=path)
                    return


            def _validate_object(schema: Mapping[str, Any], value: Any, *, path: str) -> None:
                if not isinstance(value, Mapping):
                    raise ContractValidationError(f"{path} must be an object")
                properties = schema.get("properties") or {}
                required = schema.get("required") or []
                for name in required:
                    if name not in value:
                        raise ContractValidationError(f"{path}.{name} is required")
                for name, child in properties.items():
                    if name in value:
                        _validate_schema(child, value[name], path=f"{path}.{name}")
                additional = schema.get("additionalProperties", True)
                if additional is False:
                    extra = set(value) - set(properties)
                    if extra:
                        raise ContractValidationError(f"{path} has unexpected keys: {sorted(extra)!r}")
                elif isinstance(additional, Mapping):
                    for name, child in value.items():
                        if name not in properties:
                            _validate_schema(additional, child, path=f"{path}.{name}")


            def _validate_any(schemas: Sequence[Mapping[str, Any]], value: Any, *, path: str) -> None:
                errors = []
                for schema in schemas:
                    try:
                        _validate_schema(schema, value, path=path)
                        return
                    except ContractValidationError as exc:
                        errors.append(str(exc))
                raise ContractValidationError(f"{path} does not match anyOf: {errors}")


            def _validate_one(schemas: Sequence[Mapping[str, Any]], value: Any, *, path: str) -> None:
                matches = 0
                errors = []
                for schema in schemas:
                    try:
                        _validate_schema(schema, value, path=path)
                        matches += 1
                    except ContractValidationError as exc:
                        errors.append(str(exc))
                if matches != 1:
                    raise ContractValidationError(f"{path} must match exactly one schema; matches={matches}; errors={errors}")


            def _resolve_ref(ref: str) -> Mapping[str, Any]:
                prefix = "#/components/schemas/"
                if not ref.startswith(prefix):
                    raise ContractValidationError(f"unsupported ref: {ref}")
                name = ref.removeprefix(prefix)
                try:
                    return COMPONENT_SCHEMAS[name]
                except KeyError as exc:
                    raise ContractValidationError(f"missing schema ref: {ref}") from exc


            def _is_sequence(value: Any) -> bool:
                return isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray)
            '''
        ).lstrip()
    )

