#!/usr/bin/env python3
"""Generate the current Flask route inventory as an OpenAPI 3.1 contract."""

from __future__ import annotations

import ast
import argparse
import json
import re
from collections import defaultdict
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "docs" / "architecture" / "OPENAPI.yaml"
PREFIXES = {
    "meta_data": "/api/meta",
    "data_interface": "/api/interface",
    "graph": "/api/graph",
    "system": "/api/system",
    "data_source": "/api/datasource",
    "data_flow": "/api/dataflow",
    "business_domain": "/api/bd",
    "data_factory": "/api/datafactory",
    "data_service": "/api/dataservice",
}


def quoted(value: str) -> str:
    return json.dumps(value, ensure_ascii=False)


def route_path(raw_path: str) -> tuple[str, list[dict[str, str]]]:
    parameters: list[dict[str, str]] = []

    def replace(match: re.Match[str]) -> str:
        converter = match.group(1) or "string"
        name = match.group(2)
        schema_type = "integer" if converter in {"int", "float"} else "string"
        parameters.append({"name": name, "type": schema_type})
        return "{" + name + "}"

    return re.sub(r"<(?:([a-zA-Z_]+):)?([a-zA-Z_][a-zA-Z0-9_]*)>", replace, raw_path), parameters


def extract_routes() -> list[dict[str, object]]:
    routes: list[dict[str, object]] = []
    for module, prefix in PREFIXES.items():
      for route_file in sorted((ROOT / "app" / "api" / module).glob("*.py")):
        if route_file.name == "__init__.py":
            continue
        tree = ast.parse(route_file.read_text(encoding="utf-8"))
        for node in tree.body:
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            for decorator in node.decorator_list:
                if not isinstance(decorator, ast.Call):
                    continue
                function = decorator.func
                if not (
                    isinstance(function, ast.Attribute)
                    and function.attr == "route"
                    and isinstance(function.value, ast.Name)
                    and function.value.id == "bp"
                    and decorator.args
                ):
                    continue
                raw_path = ast.literal_eval(decorator.args[0])
                methods = ["GET"]
                for keyword in decorator.keywords:
                    if keyword.arg == "methods":
                        methods = ast.literal_eval(keyword.value)
                path, parameters = route_path(prefix + raw_path)
                summary = (ast.get_docstring(node) or node.name.replace("_", " ")).splitlines()[0]
                for method in methods:
                    routes.append(
                        {
                            "path": path,
                            "method": method.lower(),
                            "tag": module,
                            "operation_id": f"{module}_{node.name}_{method.lower()}",
                            "summary": summary,
                            "parameters": parameters,
                        }
                    )
    return sorted(routes, key=lambda item: (str(item["path"]), str(item["method"])))


def render(routes: list[dict[str, object]]) -> str:
    by_path: dict[str, list[dict[str, object]]] = defaultdict(list)
    for route in routes:
        by_path[str(route["path"])].append(route)

    lines = [
        "openapi: 3.1.0",
        "info:",
        '  title: "DataOps Platform API（当前代码基线）"',
        '  version: "2026-07-16"',
        '  description: "由 scripts/generate_openapi.py 从 app/api/*/routes.py 生成。请求体与响应细节仍以现有专项 API 文档和代码为准。"',
        f"x-route-count: {len(routes)}",
        "servers:",
        '  - url: "http://localhost:15500"',
        '    description: "全本地隔离测试后端"',
        "tags:",
    ]
    for tag in PREFIXES:
        lines.extend([f"  - name: {tag}", f"    description: {quoted(PREFIXES[tag])}"])
    lines.append("paths:")

    for path, operations in sorted(by_path.items()):
        lines.append(f"  {quoted(path)}:")
        for operation in operations:
            lines.extend(
                [
                    f"    {operation['method']}:",
                    f"      tags: [{operation['tag']}]",
                    f"      operationId: {operation['operation_id']}",
                    f"      summary: {quoted(str(operation['summary']))}",
                    f"      x-source: {quoted('app/api/' + str(operation['tag']) + '/routes.py')}",
                ]
            )
            parameters = operation["parameters"]
            if parameters:
                lines.append("      parameters:")
                for parameter in parameters:
                    lines.extend(
                        [
                            f"        - name: {parameter['name']}",
                            "          in: path",
                            "          required: true",
                            "          schema:",
                            f"            type: {parameter['type']}",
                        ]
                    )
            if operation["method"] in {"post", "put", "patch"}:
                lines.extend(
                    [
                        "      requestBody:",
                        "        required: false",
                        "        content:",
                        "          application/json:",
                        "            schema:",
                        "              type: object",
                        "              additionalProperties: true",
                    ]
                )
            lines.extend(
                [
                    "      responses:",
                    '        "200":',
                    '          description: "请求已由当前实现处理"',
                    "          content:",
                    "            application/json:",
                    "              schema:",
                    "                $ref: '#/components/schemas/ApiEnvelope'",
                    "        default:",
                    '          description: "错误响应"',
                    "          content:",
                    "            application/json:",
                    "              schema:",
                    "                $ref: '#/components/schemas/ApiEnvelope'",
                ]
            )

    lines.extend(
        [
            "components:",
            "  schemas:",
            "    ApiEnvelope:",
            "      type: object",
            "      additionalProperties: true",
            "      properties:",
            "        success:",
            "          type: boolean",
            "        code:",
            "          type: integer",
            "        message:",
            "          type: string",
            "        data: {}",
            "        timestamp:",
            "          type: integer",
            "  securitySchemes:",
            "    bearerAuth:",
            "      type: http",
            "      scheme: bearer",
            "      bearerFormat: JWT",
            "      description: \"下一阶段统一认证方案；当前路由尚未全部接入。\"",
        ]
    )
    return "\n".join(lines) + "\n"


def main(output: Path = OUTPUT) -> None:
    routes = extract_routes()
    if not routes:
        raise SystemExit("No Flask routes found")
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(render(routes), encoding="utf-8")
    try:
        label = output.relative_to(ROOT)
    except ValueError:
        label = output
    print(f"Generated {label} with {len(routes)} operations")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=OUTPUT)
    args = parser.parse_args()
    main(args.output)
