#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any


TOOL_PATH = "tools/abi_framework/generator_sdk/managed_api_codegen.py"

try:
    import abi_codegen_core  # noqa: F401 — installed via pip
except ImportError:
    _CORE_SRC = Path(__file__).resolve().parents[1] / "abi_codegen_core" / "src"
    if str(_CORE_SRC) not in sys.path:
        sys.path.insert(0, str(_CORE_SRC))

from abi_codegen_core.common import load_json_object, write_if_changed


def require_str(obj: dict[str, Any], key: str, context: str) -> str:
    value = obj.get(key)
    if not isinstance(value, str) or not value:
        raise SystemExit(f"{context}: '{key}' must be a non-empty string")
    return value


def require_list(obj: dict[str, Any], key: str, context: str) -> list[Any]:
    value = obj.get(key)
    if not isinstance(value, list):
        raise SystemExit(f"{context}: '{key}' must be an array")
    return value


def require_string_list(obj: dict[str, Any], key: str, context: str) -> list[str]:
    raw = require_list(obj, key, context)
    result: list[str] = []
    for index, item in enumerate(raw):
        if not isinstance(item, str):
            raise SystemExit(f"{context}.{key}[{index}] must be a string")
        result.append(item)
    return result


def indent(lines: list[str], spaces: int) -> list[str]:
    prefix = " " * spaces
    return [prefix + line if line else "" for line in lines]


def normalize_block(lines: list[str]) -> list[str]:
    non_empty = [line for line in lines if line.strip()]
    if not non_empty:
        return lines

    min_indent = min(len(line) - len(line.lstrip(" ")) for line in non_empty)
    if min_indent <= 0:
        return lines
    return [line[min_indent:] if len(line) >= min_indent else "" for line in lines]


def collect_idl_function_names(idl: dict[str, Any]) -> set[str]:
    functions = idl.get("functions")
    if not isinstance(functions, list):
        raise SystemExit("IDL missing array 'functions'")

    names: set[str] = set()
    for index, item in enumerate(functions):
        if not isinstance(item, dict):
            raise SystemExit(f"IDL function at index {index} must be object")
        name = item.get("name")
        if not isinstance(name, str) or not name:
            raise SystemExit(f"IDL function at index {index} missing non-empty 'name'")
        names.add(name)
    return names


def validate_required_native_functions(schema: dict[str, Any], idl_names: set[str]) -> None:
    required = schema.get("required_native_functions", [])
    if not isinstance(required, list):
        raise SystemExit("managed_api.required_native_functions must be an array")

    required_names: list[str] = []
    for index, item in enumerate(required):
        if not isinstance(item, str) or not item:
            raise SystemExit(f"managed_api.required_native_functions[{index}] must be a non-empty string")
        required_names.append(item)

    missing = sorted(set(required_names) - idl_names)
    if missing:
        raise SystemExit(
            "managed_api.required_native_functions references unknown ABI functions: " + ", ".join(missing)
        )


def get_managed_namespace(schema: dict[str, Any]) -> str:
    return require_str(schema, "namespace", "managed_api")


def render_assignment(delegate_field: str, assignment_lines: list[str], context: str) -> list[str]:
    if not assignment_lines:
        raise SystemExit(f"{context}.assignment_lines must not be empty")
    assignment_lines = normalize_block(assignment_lines)
    if len(assignment_lines) == 1:
        return [f"{delegate_field} = {assignment_lines[0]};"]
    rendered = [f"{delegate_field} = {assignment_lines[0]}"]
    continuation = assignment_lines[1:]
    if continuation and continuation[0].strip() and not continuation[0].lstrip().startswith("{"):
        continuation = [f"    {line}" if line else line for line in continuation]
    rendered.extend(continuation)
    rendered[-1] = rendered[-1] + ";"
    return rendered


def render_callbacks(schema: dict[str, Any]) -> str:
    namespace_name = get_managed_namespace(schema)
    callback_classes = schema.get("callbacks")
    if not isinstance(callback_classes, list):
        raise SystemExit("managed_api.callbacks must be an array")

    lines: list[str] = [
        "// <auto-generated />",
        f"// Generated by {TOOL_PATH}",
        "#nullable enable",
        f"namespace {namespace_name};",
        "",
    ]

    seen_classes: set[str] = set()
    for class_index, class_item in enumerate(callback_classes):
        class_context = f"managed_api.callbacks[{class_index}]"
        if not isinstance(class_item, dict):
            raise SystemExit(f"{class_context} must be an object")
        class_name = require_str(class_item, "class", class_context)
        if class_name in seen_classes:
            raise SystemExit(f"Duplicate callback class '{class_name}'")
        seen_classes.add(class_name)
        summary = require_str(class_item, "summary", class_context)
        native_struct = require_str(class_item, "native_struct", class_context)
        fields = require_list(class_item, "fields", class_context)

        lines.extend([
            "/// <summary>",
            f"/// {summary}",
            "/// </summary>",
            f"public sealed class {class_name}",
            "{",
        ])

        for field_index, field_item in enumerate(fields):
            field_context = f"{class_context}.fields[{field_index}]"
            if not isinstance(field_item, dict):
                raise SystemExit(f"{field_context} must be an object")
            managed_type = require_str(field_item, "managed_type", field_context)
            managed_name = require_str(field_item, "managed_name", field_context)
            lines.append(f"    public {managed_type} {managed_name};")

        lines.append("")

        for field_index, field_item in enumerate(fields):
            field_context = f"{class_context}.fields[{field_index}]"
            delegate_type = require_str(field_item, "delegate_type", field_context)
            delegate_field = require_str(field_item, "delegate_field", field_context)
            lines.append(f"    private {delegate_type}? {delegate_field};")

        lines.extend([
            "",
            f"    internal {native_struct} BuildNative()",
            "    {",
        ])

        for field_index, field_item in enumerate(fields):
            field_context = f"{class_context}.fields[{field_index}]"
            delegate_field = require_str(field_item, "delegate_field", field_context)
            assignment_lines = require_string_list(field_item, "assignment_lines", field_context)
            rendered_assignment = render_assignment(delegate_field, assignment_lines, field_context)
            lines.extend(indent(rendered_assignment, 8))

        lines.extend([
            "",
            f"        return new {native_struct}",
            "        {",
        ])

        for field_index, field_item in enumerate(fields):
            field_context = f"{class_context}.fields[{field_index}]"
            native_field = require_str(field_item, "native_field", field_context)
            delegate_field = require_str(field_item, "delegate_field", field_context)
            lines.append(f"            {native_field} = {delegate_field},")

        lines.extend([
            "        };",
            "    }",
            "}",
            "",
        ])

    return "\n".join(lines)


def render_methods(method_items: list[Any], context: str, class_indent: int) -> list[str]:
    lines: list[str] = []
    for method_index, method_item in enumerate(method_items):
        method_context = f"{context}[{method_index}]"
        if not isinstance(method_item, dict):
            raise SystemExit(f"{method_context} must be an object")

        if "line" in method_item:
            line = require_str(method_item, "line", method_context)
            lines.extend(indent([line], class_indent))
            lines.append("")
            continue

        if "signature" in method_item:
            signature = require_str(method_item, "signature", method_context)
            body = require_string_list(method_item, "body", method_context)
            body = normalize_block(body)
            lines.extend(indent([signature, "{"], class_indent))
            lines.extend(indent(body, class_indent + 4))
            lines.extend(indent(["}"], class_indent))
            lines.append("")
            continue

        if "lines" in method_item:
            raw_lines = require_string_list(method_item, "lines", method_context)
            raw_lines = normalize_block(raw_lines)
            lines.extend(indent(raw_lines, class_indent))
            lines.append("")
            continue

        raise SystemExit(f"{method_context} must define one of: line, signature, lines")

    return lines


def render_partial_class_section(schema: dict[str, Any], schema_key: str) -> str:
    """Render a ``public sealed partial class`` section driven by a metadata key.

    Both the ``builder`` and ``peer_connection_async`` sections share the same
    structure: a single dict with ``class`` and ``methods`` keys.  This helper
    handles both so the logic lives in exactly one place.
    """
    namespace_name = get_managed_namespace(schema)
    spec = schema.get(schema_key)
    if not isinstance(spec, dict):
        raise SystemExit(f"managed_api.{schema_key} must be an object")

    context = f"managed_api.{schema_key}"
    class_name = require_str(spec, "class", context)
    methods = require_list(spec, "methods", context)

    lines: list[str] = [
        "// <auto-generated />",
        f"// Generated by {TOOL_PATH}",
        "#nullable enable",
        f"namespace {namespace_name};",
        "",
        f"public sealed partial class {class_name}",
        "{",
    ]

    lines.extend(render_methods(methods, f"{context}.methods", 4))
    lines.append("}")
    lines.append("")
    return "\n".join(lines)


def render_handle_api(schema: dict[str, Any]) -> str:
    namespace_name = get_managed_namespace(schema)
    handle_classes = schema.get("handle_api")
    if not isinstance(handle_classes, list):
        raise SystemExit("managed_api.handle_api must be an array")

    lines: list[str] = [
        "// <auto-generated />",
        f"// Generated by {TOOL_PATH}",
        "#nullable enable",
        f"namespace {namespace_name};",
        "",
    ]

    seen_classes: set[str] = set()
    for class_index, class_item in enumerate(handle_classes):
        class_context = f"managed_api.handle_api[{class_index}]"
        if not isinstance(class_item, dict):
            raise SystemExit(f"{class_context} must be an object")
        class_name = require_str(class_item, "class", class_context)
        if class_name in seen_classes:
            raise SystemExit(f"Duplicate handle_api class '{class_name}'")
        seen_classes.add(class_name)
        members = require_list(class_item, "members", class_context)

        lines.extend([
            f"public sealed partial class {class_name}",
            "{",
        ])
        lines.extend(render_methods(members, f"{class_context}.members", 4))
        lines.append("}")
        lines.append("")

    return "\n".join(lines)


def render_native_impl_handles(schema: dict[str, Any]) -> str:
    handles = schema.get("native_impl_handles")
    if not isinstance(handles, list):
        raise SystemExit("managed_api.native_impl_handles must be an array")

    lines: list[str] = [
        "// <auto-generated />",
        f"// Generated by {TOOL_PATH}",
        "#pragma once",
        "",
    ]

    seen_names: set[str] = set()
    for index, item in enumerate(handles):
        context = f"managed_api.native_impl_handles[{index}]"
        if not isinstance(item, dict):
            raise SystemExit(f"{context} must be an object")
        name = require_str(item, "name", context)
        if name in seen_names:
            raise SystemExit(f"Duplicate native_impl_handles entry '{name}'")
        seen_names.add(name)
        fields = require_string_list(item, "fields", context)

        lines.append(f"struct {name} {{")
        for field in fields:
            lines.append(f"  {field}")
        lines.append("};")
        lines.append("")

    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--idl", required=True)
    parser.add_argument("--managed-api", required=True)
    parser.add_argument("--out-callbacks")
    parser.add_argument("--out-builder")
    parser.add_argument("--out-handle-api")
    parser.add_argument("--out-peer-async")
    parser.add_argument("--out-native-handles")
    parser.add_argument("--check", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    idl = load_json_object(Path(args.idl))
    schema = load_json_object(Path(args.managed_api))

    schema_version = schema.get("schema_version")
    if schema_version != 2:
        raise SystemExit(f"managed_api.schema_version must be 2, got {schema_version!r}")

    idl_names = collect_idl_function_names(idl)
    validate_required_native_functions(schema, idl_names)

    status = 0
    if args.out_callbacks:
        status |= write_if_changed(Path(args.out_callbacks), render_callbacks(schema), args.check, args.dry_run)
    if args.out_builder:
        status |= write_if_changed(Path(args.out_builder), render_partial_class_section(schema, "builder"), args.check, args.dry_run)
    if args.out_handle_api:
        status |= write_if_changed(Path(args.out_handle_api), render_handle_api(schema), args.check, args.dry_run)
    if args.out_peer_async:
        status |= write_if_changed(Path(args.out_peer_async), render_partial_class_section(schema, "peer_connection_async"), args.check, args.dry_run)
    if args.out_native_handles:
        native_handles_source = render_native_impl_handles(schema)
        status |= write_if_changed(Path(args.out_native_handles), native_handles_source, args.check, args.dry_run)

    if not any([
        args.out_callbacks,
        args.out_builder,
        args.out_handle_api,
        args.out_peer_async,
        args.out_native_handles,
    ]):
        raise SystemExit("At least one output path must be provided.")

    return status


if __name__ == "__main__":
    raise SystemExit(main())
