#!/usr/bin/env python3
"""Validate component boundary rules for pure architectural targets."""

from __future__ import annotations

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

INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""")
SINGLETON_RE = re.compile(r"\b(?:App::I|Canvas::I)\b")

REPO_ROOT = Path(__file__).resolve().parents[2]
SRC_ROOT = REPO_ROOT / "src"
CMAKE_FILE = REPO_ROOT / "CMakeLists.txt"

COMPONENT_BY_DIR = {
    "foundation": "pp_foundation",
    "assets": "pp_assets",
    "paint": "pp_paint",
    "document": "pp_document",
    "renderer_api": "pp_renderer_api",
    "paint_renderer": "pp_paint_renderer",
    "ui_core": "pp_ui_core",
    "app_core": "pp_app_core",
}

PURE_TARGETS = set(COMPONENT_BY_DIR.values())
TARGET_INFRA = {"pp_project_options", "pp_project_warnings", "pp_xml_tinyxml2"}

ALLOWED_LINKS = {
    "pp_foundation": set(),
    "pp_assets": {"pp_foundation"},
    "pp_paint": {"pp_foundation"},
    "pp_document": {"pp_foundation", "pp_assets", "pp_paint"},
    "pp_renderer_api": {"pp_foundation"},
    "pp_paint_renderer": {"pp_foundation", "pp_paint", "pp_document", "pp_renderer_api"},
    "pp_ui_core": {"pp_foundation", "pp_xml_tinyxml2"},
    "pp_app_core": {"pp_foundation", "pp_document", "pp_assets", "pp_paint", "pp_ui_core"},
}

ALLOWED_LOCAL_INCLUDES = {
    "pp_foundation": ("foundation/",),
    "pp_assets": ("foundation/", "assets/"),
    "pp_paint": ("foundation/", "paint/"),
    "pp_document": ("foundation/", "assets/", "paint/", "document/"),
    "pp_renderer_api": ("foundation/", "renderer_api/"),
    "pp_paint_renderer": (
        "assets/",
        "document/",
        "foundation/",
        "paint/",
        "paint_renderer/",
        "renderer_api/",
    ),
    "pp_ui_core": ("foundation/", "ui_core/"),
    "pp_app_core": ("app_core/", "assets/", "document/", "foundation/", "paint/", "ui_core/"),
}

ALLOWED_EXTERNAL_PREFIXES = (
    "stb/",
)

FORBIDDEN_INCLUDE_TOKENS = (
    "platform/windows",
    "platform/windows",
    "platform_apple",
    "platform_legacy",
    "platform_api/",
    "platform_legacy/",
    "platform_apple/",
    "platform_windows/",
    "opengl/",
    "/opengl",
    "<gl",
    "glad/",
    "<GL/",
    "<openGL/",
    "vulkan/",
    "directx",
    "d3d",
    "android/",
    "ios/",
    "objc/",
    "metal/",
    "windows.h",
    "wingdi.h",
    "afxwin.h",
    "App::I",
    "Canvas::I",
)


def repo_root() -> Path:
    return REPO_ROOT


def component_for_path(path: Path) -> str | None:
    try:
        rel = path.relative_to(SRC_ROOT)
    except ValueError:
        return None

    if not rel.parts:
        return None
    return COMPONENT_BY_DIR.get(rel.parts[0])


def collect_target_links(cmake_text: str, target: str) -> list[str] | None:
    pattern = re.compile(rf"target_link_libraries\(\s*{re.escape(target)}\s+(.*?)\)", re.S | re.I)
    matches = pattern.findall(cmake_text)
    if not matches:
        return None

    tokens: list[str] = []
    for block in matches:
        block = block.replace("\\\n", " ")
        for token in re.split(r"\s+", block):
            token = token.strip()
            if not token or token.upper() in {"PUBLIC", "PRIVATE", "INTERFACE"}:
                continue
            token = token.strip("\"'")
            if token.startswith("$<") or token.startswith("SHELL:"):
                continue
            tokens.append(token)
    return tokens


def check_link_dependencies(cmake_text: str) -> list[dict[str, Any]]:
    violations: list[dict[str, Any]] = []
    for target in sorted(PURE_TARGETS):
        deps = collect_target_links(cmake_text, target)
        if deps is None:
            violations.append(
                {
                    "target": target,
                    "dependency": None,
                    "kind": "missing-target-link-declaration",
                    "message": "No target_link_libraries block found",
                }
            )
            continue

        allowed = ALLOWED_LINKS[target]
        for dependency in deps:
            if dependency in TARGET_INFRA:
                continue
            if dependency in allowed:
                continue
            if dependency.startswith("pp_"):
                violations.append(
                    {
                        "target": target,
                        "dependency": dependency,
                        "kind": "invalid-target-edge",
                        "message": f"{target} must not depend on {dependency}",
                    }
                )
    return violations


def is_forbidden_include(component: str, include: str) -> tuple[bool, str | None]:
    include_lower = include.lower()
    if any(token in include_lower for token in FORBIDDEN_INCLUDE_TOKENS):
        token = next(token for token in FORBIDDEN_INCLUDE_TOKENS if token in include_lower)
        return True, token
    if "/" in include_lower and any(include_lower.startswith(prefix) for prefix in ALLOWED_EXTERNAL_PREFIXES):
        return False, None

    if "/" in include:
        allowed_prefixes = ALLOWED_LOCAL_INCLUDES[component]
        if not any(include_lower.startswith(prefix) for prefix in allowed_prefixes):
            return True, "component-boundary-crossing-include"
    return False, None


def check_pure_component_sources() -> list[dict[str, Any]]:
    violations: list[dict[str, Any]] = []

    for path in SRC_ROOT.rglob("*"):
        if not path.is_file():
            continue
        if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}:
            continue

        component = component_for_path(path)
        if component is None:
            continue

        for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
            include_match = INCLUDE_RE.match(line)
            if include_match:
                include = (include_match.group(2) or include_match.group(3) or "").strip()
                forbidden, reason = is_forbidden_include(component, include)
                if forbidden:
                    violations.append(
                        {
                            "file": str(path.relative_to(REPO_ROOT)),
                            "line": line_no,
                            "kind": "forbidden-include",
                            "include": include,
                            "detail": reason,
                            "text": line.strip(),
                        }
                    )

            if SINGLETON_RE.search(line):
                violations.append(
                    {
                        "file": str(path.relative_to(REPO_ROOT)),
                        "line": line_no,
                        "kind": "legacy-singleton-reference",
                        "detail": "App::I/Canvas::I is not allowed in pure components",
                        "text": line.strip(),
                    }
                )

    return violations


def main() -> int:
    source_violations = check_pure_component_sources()
    cmake_text = CMAKE_FILE.read_text(encoding="utf-8")
    link_violations = check_link_dependencies(cmake_text)

    all_violations = source_violations + link_violations
    ok = len(all_violations) == 0
    print(
        json.dumps(
            {
                "ok": ok,
                "summary": {
                    "sourceViolationCount": len(source_violations),
                    "linkViolationCount": len(link_violations),
                },
                "violations": all_violations,
            },
            separators=(",", ":"),
        )
    )
    return 0 if ok else 1


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