"""Bounded local Xcode graph traversal; no Xcode process or provider requests."""
from __future__ import annotations

import json
from pathlib import Path
import plistlib
import subprocess
import xml.etree.ElementTree as ET

PACKAGES = {"XCRemoteSwiftPackageReference", "XCLocalSwiftPackageReference", "XCSwiftPackageProductDependency"}


class Refused(ValueError):
    """Only fixed codes cross the guard's public boundary."""


def owned(root: Path, path: Path, *, missing: bool = False) -> Path:
    result = path.resolve(strict=not missing)
    if not result.is_relative_to(root):
        raise Refused("native_workspace_reference_unverified")
    return result


def selected(root: Path, container: str, before_pods: bool) -> Path:
    if not container:
        choices = sorted(root.glob("*.xcworkspace")) or sorted(root.glob("*.xcodeproj"))
        if len(choices) != 1:
            raise Refused("native_project_selection_required")
        return owned(root, choices[0])
    path = owned(root, root / container, missing=before_pods)
    if not path.exists() and before_pods and path.suffix == ".xcworkspace":
        path = path.with_suffix(".xcodeproj")
    return owned(root, path)


def document(path: Path, maximum: int) -> bytes:
    if path.is_symlink() or not path.is_file() or path.stat().st_size > maximum:
        raise Refused("native_project_parse_unverified")
    return path.read_bytes()


def workspace(root: Path, path: Path, before_pods: bool, initial_pods_lock: bool) -> list[Path]:
    try:
        tree = ET.fromstring(document(path / "contents.xcworkspacedata", 1048576))
    except ET.ParseError:
        raise Refused("native_workspace_reference_unverified") from None
    pending, found, count = [(tree, path.parent, 0)], [], 0
    while pending:
        node, parent, depth = pending.pop()
        count += 1
        if count > 10000 or depth > 20:
            raise Refused("native_workspace_reference_unverified")
        kind, sep, value = node.get("location", "group:").partition(":")
        if not sep or kind not in {"group", "container", "self", "absolute"}:
            raise Refused("native_workspace_reference_unverified")
        target = owned(root, (parent if kind == "group" else path.parent) / value, missing=True)
        if node.tag == "FileRef" and (target.name == "Package.swift" or (target / "Package.swift").is_file()):
            raise Refused("native_package_proxy_transport_required")
        if node.tag == "FileRef" and target.suffix in {".xcodeproj", ".xcworkspace"}:
            pods = path.parent / "Pods/Pods.xcodeproj"
            deferred = (before_pods and target == pods and not target.exists()
                        and (path.parent / "Podfile").is_file()
                        and (initial_pods_lock or (path.parent / "Podfile.lock").is_file()))
            if not deferred:
                found.append(owned(root, target))
        pending.extend((child, target, depth + 1) for child in node)
    return found + schemes(root, path)


def objects(path: Path) -> dict:
    body = document(path / "project.pbxproj", 16 * 1024 * 1024)
    try:
        value = plistlib.loads(body)
    except (ValueError, plistlib.InvalidFileException):
        try:
            result = subprocess.run(["plutil", "-convert", "json", "-o", "-", "--", str(path / "project.pbxproj")],
                                    capture_output=True, timeout=15, check=False)
            value = json.loads(result.stdout) if result.returncode == 0 else None
        except (OSError, ValueError, subprocess.SubprocessError):
            value = None
    rows = value.get("objects") if isinstance(value, dict) else None
    if (not isinstance(rows, dict) or not rows or len(rows) > 100000
            or not all(isinstance(r, dict) for r in rows.values())):
        raise Refused("native_project_parse_unverified")
    return rows


def reference(root: Path, path: Path, rows: dict, key: str, parents: dict, chain: tuple = ()) -> Path:
    if key in chain or len(chain) > 20 or key not in rows:
        raise Refused("native_workspace_reference_unverified")
    row = rows[key]
    tree, value = row.get("sourceTree", "<group>"), row.get("path", "")
    if not isinstance(value, str) or "$" in value:
        raise Refused("native_workspace_reference_unverified")
    if tree in {"SOURCE_ROOT", "<absolute>"}:
        base = path.parent
    elif tree == "<group>":
        candidates = parents.get(key, [])
        if len(candidates) > 1:
            raise Refused("native_workspace_reference_unverified")
        base = reference(root, path, rows, candidates[0], parents, (*chain, key)) if candidates else path.parent
    else:
        raise Refused("native_workspace_reference_unverified")
    return owned(root, base / value, missing=True)


def project(root: Path, path: Path) -> list[Path]:
    rows, parents, found = objects(path), {}, []
    for key, row in rows.items():
        if not isinstance(row.get("isa"), str):
            raise Refused("native_project_parse_unverified")
        if row.get("isa") in PACKAGES or row.get("packageReferences") or row.get("packageProductDependencies"):
            raise Refused("native_package_proxy_transport_required")
        if row.get("isa") == "PBXProject" and any(row.get(key, "") not in ("", ".")
                                                 for key in ("projectDirPath", "projectRoot")):
            raise Refused("native_workspace_reference_unverified")
        children = row.get("children", [])
        if not isinstance(children, list) or not all(isinstance(child, str) for child in children):
            raise Refused("native_project_parse_unverified")
        for child in children:
            parents.setdefault(child, []).append(key)
    for key, row in rows.items():
        is_project = (str(row.get("path", "")).endswith(".xcodeproj")
                      or row.get("lastKnownFileType") == "wrapper.pb-project")
        if row.get("isa") == "PBXFileReference" and is_project:
            found.append(owned(root, reference(root, path, rows, key, parents)))
    embedded = path / "project.xcworkspace"
    if embedded.exists():
        found.append(owned(root, embedded))
    return found + schemes(root, path)


def schemes(root: Path, container: Path) -> list[Path]:
    paths = list(container.glob("xcshareddata/xcschemes/*.xcscheme"))
    if len(paths) > 200:
        raise Refused("native_workspace_reference_unverified")
    found = []
    for path in paths:
        try:
            tree = ET.fromstring(document(owned(root, path), 1048576))
        except ET.ParseError:
            raise Refused("native_workspace_reference_unverified") from None
        for node in tree.iter("BuildableReference"):
            kind, sep, value = node.get("ReferencedContainer", "").partition(":")
            if kind != "container" or not sep:
                raise Refused("native_workspace_reference_unverified")
            found.append(owned(root, container.parent / value))
    return found
