#!/usr/bin/env python3
"""Library for validating and resolving evidence-backed framework profiles."""

import json
from pathlib import Path

from lookup import (
    normalize,
    search_exact,
    slice_entry,
)


SKILL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_INDEX = SKILL_ROOT / "data" / "framework_index.json"
REQUIRED_FRAMEWORK_KEYS = {
    "qualified_name",
    "scope",
    "module",
    "kind",
    "lifecycle",
    "configuration",
    "hooks",
    "state_apis",
    "execution",
    "result_apis",
    "contracts",
}
REFERENCE_SECTIONS = (
    "configuration",
    "hooks",
    "state_apis",
    "execution",
    "result_apis",
)


def load_index(index_path):
    try:
        data = json.loads(index_path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as error:
        raise ValueError(f"failed to load {index_path}: {error}") from error
    if not isinstance(data, dict):
        raise ValueError("framework index root must be an object")
    if data.get("schema_version") != 1:
        raise ValueError("unsupported framework index schema_version")
    frameworks = data.get("frameworks")
    if not isinstance(frameworks, list):
        raise ValueError("frameworks must be an array")
    return data


def evidence_set(item):
    evidence = item.get("evidence", []) if isinstance(item, dict) else []
    if not isinstance(evidence, list):
        return set()
    return {str(source) for source in evidence}


def api_key(row):
    return f"{row.get('page', '')}#{row.get('anchor', '')}"


def indexed_source_keys(rows):
    return {api_key(row) for row in rows if row.get("page") and row.get("anchor")}


def validate_source_ids(data, rows):
    known_sources = indexed_source_keys(rows)
    errors = []
    for framework_index, framework in enumerate(data.get("frameworks", [])):
        if not isinstance(framework, dict):
            errors.append(f"frameworks[{framework_index}] must be an object")
            continue
        for source in evidence_set(framework):
            if source not in known_sources:
                errors.append(
                    f"frameworks[{framework_index}].evidence references unknown source {source}"
                )
        for section in ("lifecycle", "contracts"):
            entries = framework.get(section, [])
            if not isinstance(entries, list):
                continue
            for entry_index, entry in enumerate(entries):
                for source in evidence_set(entry):
                    if source not in known_sources:
                        errors.append(
                            f"{section}[{entry_index}].evidence references unknown source {source}"
                        )
    return errors


def resolve_reference(reference, framework, rows, codegen_root, path):
    if not isinstance(reference, dict) or not reference.get("api"):
        return [f"{path} must contain an api"]
    qualified_name = reference["api"]
    matches = [
        row
        for row in search_exact(rows, qualified_name)
        if normalize(row.get("scope", "")) == normalize(framework["scope"])
        and normalize(row.get("module", "")) == normalize(framework["module"])
    ]
    if not matches:
        return [
            f"{path}.api {qualified_name!r} is not indexed in "
            f"{framework['scope']}/{framework['module']}"
        ]

    evidence = evidence_set(reference)
    if not evidence:
        return [f"{path}.evidence must contain at least one source id"]
    matching_evidence = [row for row in matches if api_key(row) in evidence]
    if not matching_evidence:
        expected = ", ".join(api_key(row) for row in matches)
        return [
            f"{path}.evidence does not identify an indexed overload; expected one of {expected}"
        ]

    for row in matching_evidence:
        if not slice_entry(
            codegen_root, row["page"], row["signature"], row["anchor"]
        ):
            return [
                f"{path}.api {qualified_name!r} points to a missing reference body "
                f"{api_key(row)}"
            ]
    return []


def validate_framework(framework, rows, codegen_root, index):
    errors = []
    if not isinstance(framework, dict):
        return [f"frameworks[{index}] must be an object"]
    missing = sorted(REQUIRED_FRAMEWORK_KEYS - set(framework))
    if missing:
        errors.append(f"frameworks[{index}] missing keys: {', '.join(missing)}")
        return errors
    if framework.get("kind") != "framework":
        errors.append(f"frameworks[{index}].kind must be 'framework'")
    root_matches = [
        row
        for row in search_exact(rows, framework["qualified_name"])
        if row.get("kind") == "class"
        and normalize(row.get("scope", "")) == normalize(framework["scope"])
        and normalize(row.get("module", "")) == normalize(framework["module"])
    ]
    if not root_matches:
        errors.append(
            f"{framework['qualified_name']} is not an indexed class in "
            f"{framework['scope']}/{framework['module']}"
        )

    for section in REFERENCE_SECTIONS:
        entries = framework.get(section)
        if not isinstance(entries, list):
            errors.append(f"{framework['qualified_name']}.{section} must be an array")
            continue
        for entry_index, entry in enumerate(entries):
            errors.extend(
                resolve_reference(
                    entry,
                    framework,
                    rows,
                    codegen_root,
                    f"{framework['qualified_name']}.{section}[{entry_index}]",
                )
            )

    for phase_index, phase in enumerate(framework.get("lifecycle", [])):
        if not isinstance(phase, dict):
            errors.append(
                f"{framework['qualified_name']}.lifecycle[{phase_index}] must be an object"
            )
            continue
        for member_index, member in enumerate(phase.get("members", [])):
            matches = [
                row
                for row in search_exact(rows, member)
                if normalize(row.get("scope", "")) == normalize(framework["scope"])
                and normalize(row.get("module", "")) == normalize(framework["module"])
            ]
            if not matches:
                errors.append(
                    f"{framework['qualified_name']}.lifecycle[{phase_index}].members"
                    f"[{member_index}] {member!r} is not indexed"
                )

    for contract_index, contract in enumerate(framework.get("contracts", [])):
        callback = contract.get("callback") if isinstance(contract, dict) else None
        if not callback:
            errors.append(
                f"{framework['qualified_name']}.contracts[{contract_index}] must contain callback"
            )
            continue
        matches = [
            row
            for row in search_exact(rows, callback)
            if normalize(row.get("scope", "")) == normalize(framework["scope"])
            and normalize(row.get("module", "")) == normalize(framework["module"])
        ]
        if not matches:
            errors.append(
                f"{framework['qualified_name']}.contracts[{contract_index}].callback "
                f"{callback!r} is not indexed"
            )
        for source in evidence_set(contract):
            if source not in indexed_source_keys(rows):
                errors.append(
                    f"{framework['qualified_name']}.contracts[{contract_index}] "
                    f"references unknown source {source}"
                )
    return errors


def validate_index(data, rows, codegen_root):
    errors = validate_source_ids(data, rows)
    names = set()
    for index, framework in enumerate(data.get("frameworks", [])):
        if isinstance(framework, dict):
            name = framework.get("qualified_name")
            if name in names:
                errors.append(f"duplicate framework qualified_name: {name}")
            names.add(name)
        errors.extend(validate_framework(framework, rows, codegen_root, index))
    return errors


def scaffold_packet(framework, config):
    configured_names = {
        entry["api"].rsplit(".", 1)[-1]
        for entry in framework["configuration"]
    }
    diagnostics = []
    for key in config:
        if key not in configured_names:
            diagnostics.append(
                {
                    "code": "UNKNOWN_CONFIGURATION",
                    "severity": "error",
                    "message": (
                        f"{key} is not a configuration member of "
                        f"{framework['qualified_name']}"
                    ),
                    "next_action": "Use a configuration field listed by the framework profile.",
                }
            )
    for entry in framework["configuration"]:
        if entry.get("contract_evidence_required") and entry["api"].rsplit(".", 1)[-1] in config:
            diagnostics.append(
                {
                    "code": "CONTRACT_MODE_UNRESOLVED",
                    "severity": "error",
                    "message": (
                        f"{entry['api']} is a candidate discriminator, but no "
                        "configuration-to-return-schema mapping is recorded."
                    ),
                    "next_action": "Add a mode contract before generating callback fields.",
                    "source_ids": entry.get("evidence", []),
                }
            )
    contract_status = "resolved"
    for contract in framework["contracts"]:
        if contract.get("status") != "resolved":
            contract_status = contract.get("status", "incomplete")
            diagnostics.append(
                {
                    "code": "CONTRACT_INCOMPLETE",
                    "severity": "error",
                    "message": contract.get("reason", "framework contract is incomplete"),
                    "next_action": contract.get("required_next_step", "Resolve the contract."),
                    "source_ids": contract.get("evidence", []),
                }
            )
    return {
        "status": "resolved",
        "scaffold_status": "resolved",
        "contract_status": contract_status,
        "framework": framework["qualified_name"],
        "scope": framework["scope"],
        "module": framework["module"],
        "configured_values": config,
        "lifecycle": framework["lifecycle"],
        "configuration": framework["configuration"],
        "hooks": framework["hooks"],
        "state_apis": framework["state_apis"],
        "execution": framework["execution"],
        "result_apis": framework["result_apis"],
        "diagnostics": diagnostics,
        "evidence": framework.get("evidence", []),
    }
