"""Contract validation logic for Condensa Code.

Validates components against their !:fn contracts and checks
wire compatibility between connected components.

Usage:
    from cdn.contracts import parse_contract, check, validate_wires, validate_all

    contracts = parse_contract('''
        !:fn DashboardPage /props:(programs:Program[] onSelect:fn(id:n)->void)
        !:type Program { id:n name:s* weeks:n }
        !:wire dashboard.onSelect -> programs.highlight
    ''')

    # Check a single component
    result = check("DashboardPage", props=["programs", "onSelect"], contracts=contracts)

    # Check all wires
    wire_issues = validate_wires(contracts)

    # Check everything
    all_results = validate_all(contracts, built_components={
        "DashboardPage": ["programs", "onSelect"],
        "ProgramsPage": ["programs", "onStart"],
    })
"""

from __future__ import annotations

from .types import ContractSet, CheckResult, FnContract


def check(
    component_name: str,
    *,
    props: list[str] | None = None,
    callbacks: list[str] | None = None,
    contracts: ContractSet,
) -> CheckResult:
    """Validate a built component against its !:fn contract.

    Parameters
    ----------
    component_name : str
        Name of the component to validate.
    props : list[str] | None
        Props the built component actually accepts.
    callbacks : list[str] | None
        Callbacks the built component implements.
    contracts : ContractSet
        The parsed contract set to validate against.

    Returns
    -------
    CheckResult
        Structured pass/fail result with details.
    """
    fn = contracts.get_fn(component_name)
    if fn is None:
        return CheckResult(
            passed=False,
            component=component_name,
            issues=[f"No contract found for '{component_name}'"],
        )

    actual_props = set(props or [])
    actual_callbacks = set(callbacks or [])
    # Callbacks are also props in the contract
    all_actual = actual_props | actual_callbacks

    expected_props = fn.prop_names
    expected_required = fn.required_props
    expected_callbacks = fn.callback_props

    # Found: intersection of expected and actual
    found = sorted(expected_props & all_actual)

    # Missing: expected but not actual
    missing = sorted(expected_props - all_actual)

    # Extra: actual but not expected
    extra = sorted(all_actual - expected_props)

    # Missing callbacks specifically
    missing_cbs = sorted(expected_callbacks - all_actual)

    # Type reference checks
    type_mismatches = []
    for prop in fn.props:
        # Check if referenced types exist in the contract set
        base_type = prop.type.rstrip("*?[]")
        if base_type.startswith("fn"):
            continue
        if base_type in ("s", "n", "b", "void", "any"):
            continue
        if base_type and base_type[0].isupper() and base_type not in contracts.all_type_names:
            type_mismatches.append(f"{prop.name}: type '{base_type}' not defined")

    # Determine pass/fail
    issues = []
    if missing:
        issues.append(f"Missing props: {', '.join(missing)}")
    if missing_cbs:
        issues.append(f"Missing callbacks: {', '.join(missing_cbs)}")
    if type_mismatches:
        issues.extend(type_mismatches)

    # Pass if all required props present (extras are warnings, not failures)
    missing_required = expected_required - all_actual
    passed = len(missing_required) == 0 and len(missing_cbs) == 0

    return CheckResult(
        passed=passed,
        component=component_name,
        found_props=found,
        missing_props=missing,
        extra_props=extra,
        missing_callbacks=missing_cbs,
        type_mismatches=type_mismatches,
        issues=issues,
    )


def validate_wires(contracts: ContractSet) -> list[str]:
    """Validate all !:wire connections for compatibility.

    Checks:
    1. Source component exists in contracts
    2. Target component exists in contracts
    3. Source port (callback) exists in source component's props
    4. Target port (handler) is reachable

    Returns a list of issue strings (empty = all valid).
    """
    issues = []

    for wire in contracts.wires:
        src_comp = wire.source_component
        tgt_comp = wire.target_component

        # Check source component exists
        src_fn = contracts.get_fn(src_comp)
        if src_comp not in contracts.all_component_names and src_comp not in _module_names(contracts):
            issues.append(
                f"Wire {wire.source} -> {wire.target}: "
                f"source component '{src_comp}' not found in contracts"
            )

        # Check target component exists
        tgt_fn = contracts.get_fn(tgt_comp)
        if tgt_comp not in contracts.all_component_names and tgt_comp not in _module_names(contracts):
            issues.append(
                f"Wire {wire.source} -> {wire.target}: "
                f"target component '{tgt_comp}' not found in contracts"
            )

        # Check source port exists as a prop (callback)
        if src_fn and wire.source_port:
            if wire.source_port not in src_fn.prop_names:
                issues.append(
                    f"Wire {wire.source} -> {wire.target}: "
                    f"'{wire.source_port}' not in {src_comp}'s props "
                    f"({', '.join(sorted(src_fn.prop_names))})"
                )

    return issues


def validate_all(
    contracts: ContractSet,
    built_components: dict[str, list[str]] | None = None,
) -> dict:
    """Run all validations and return a comprehensive report.

    Parameters
    ----------
    contracts : ContractSet
        Parsed contracts to validate.
    built_components : dict[str, list[str]] | None
        Map of component name -> list of props the built component accepts.
        If None, only structural validation is performed (wire checks,
        type references, internal consistency).

    Returns
    -------
    dict with keys:
        - component_checks: list of CheckResult
        - wire_issues: list of str
        - type_issues: list of str
        - passed: bool (True if all checks pass)
        - summary: str (Condensa-formatted summary)
    """
    results = {
        "component_checks": [],
        "wire_issues": [],
        "type_issues": [],
        "passed": True,
    }

    # Component checks
    if built_components:
        for comp_name, props in built_components.items():
            result = check(comp_name, props=props, contracts=contracts)
            results["component_checks"].append(result)
            if not result.passed:
                results["passed"] = False

    # Wire validation
    wire_issues = validate_wires(contracts)
    results["wire_issues"] = wire_issues
    if wire_issues:
        results["passed"] = False

    # Type reference validation
    type_issues = _validate_type_references(contracts)
    results["type_issues"] = type_issues
    if type_issues:
        results["passed"] = False

    # Summary
    n_checks = len(results["component_checks"])
    n_passed = sum(1 for r in results["component_checks"] if r.passed)
    n_wire_issues = len(wire_issues)
    n_type_issues = len(type_issues)

    if results["passed"]:
        results["summary"] = f"=:check pass components:{n_passed}/{n_checks} wires:ok types:ok !"
    else:
        results["summary"] = (
            f"=:check fail components:{n_passed}/{n_checks} "
            f"wire_issues:{n_wire_issues} type_issues:{n_type_issues} ~~"
        )

    return results


def _validate_type_references(contracts: ContractSet) -> list[str]:
    """Check that all type references in fn props resolve to defined types."""
    issues = []
    primitives = {"s", "n", "b", "void", "any"}

    for fn in contracts.functions.values():
        for prop in fn.props:
            base_type = prop.type.rstrip("*?[]")
            if base_type.startswith("fn"):
                continue
            if base_type in primitives:
                continue
            if base_type and base_type[0].isupper() and base_type not in contracts.all_type_names:
                issues.append(
                    f"{fn.name}.{prop.name}: references undefined type '{base_type}'"
                )

    return issues


def _module_names(contracts: ContractSet) -> set[str]:
    """Get all module/app names."""
    names = set(contracts.modules.keys())
    # Also consider component names that might be used as module refs in wires
    # e.g., "app.nav" where "app" is a module
    return names
