from __future__ import annotations

import json
import re
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path
from typing import Any, Iterable

import yaml
from jsonschema import Draft202012Validator
from yaml.nodes import MappingNode, SequenceNode

ID_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
REQUIRED_ROOT_FIELDS = [
    "version",
    "trust_boundaries",
    "data_classifications",
    "approved_patterns",
    "anti_patterns",
]


class SecurityContextError(RuntimeError):
    """Base exception for security-context loading and validation failures."""


class SecurityContextValidationError(SecurityContextError):
    """Raised when security-context.yaml does not satisfy the contract."""

    def __init__(self, errors: list[dict[str, Any]]) -> None:
        self.errors = errors
        first_error = errors[0]["message"] if errors else "Unknown validation error"
        super().__init__(f"Security context validation failed: {first_error}")


@dataclass(frozen=True)
class ValidationReport:
    valid: bool
    errors: list[dict[str, Any]]
    warnings: list[str]
    context: dict[str, Any] | None = None


def load_schema(schema_path: str | Path | None = None) -> dict[str, Any]:
    if schema_path is not None:
        return json.loads(Path(schema_path).read_text(encoding="utf-8"))

    resource = files("aegis").joinpath("schemas/security-context.schema.json")
    return json.loads(resource.read_text(encoding="utf-8"))


def validate_context_file(
    context_path: str | Path,
    schema_path: str | Path | None = None,
) -> ValidationReport:
    path = Path(context_path)
    if not path.exists():
        return ValidationReport(
            valid=False,
            errors=[
                {
                    "path": "/",
                    "line": None,
                    "message": f"File does not exist: {path}",
                }
            ],
            warnings=[],
            context=None,
        )

    raw_text = path.read_text(encoding="utf-8")
    line_map = _build_line_map(raw_text)

    try:
        loaded = yaml.safe_load(raw_text)
    except yaml.YAMLError as exc:
        line = None
        if hasattr(exc, "problem_mark") and exc.problem_mark is not None:
            line = exc.problem_mark.line + 1
        return ValidationReport(
            valid=False,
            errors=[
                {
                    "path": "/",
                    "line": line,
                    "message": f"YAML parse error: {exc}",
                }
            ],
            warnings=[],
            context=None,
        )

    if loaded is None:
        loaded = {}

    if not isinstance(loaded, dict):
        return ValidationReport(
            valid=False,
            errors=[
                {
                    "path": "/",
                    "line": 1,
                    "message": "Root YAML value must be an object.",
                }
            ],
            warnings=[],
            context=None,
        )

    schema = load_schema(schema_path)
    return validate_context_data(loaded, schema, line_map)


def load_and_validate_context(
    context_path: str | Path,
    schema_path: str | Path | None = None,
) -> dict[str, Any]:
    report = validate_context_file(context_path=context_path, schema_path=schema_path)
    if not report.valid or report.context is None:
        raise SecurityContextValidationError(report.errors)
    return report.context


def validate_context_data(
    context: dict[str, Any],
    schema: dict[str, Any],
    line_map: dict[tuple[Any, ...], int] | None = None,
) -> ValidationReport:
    if line_map is None:
        line_map = {}

    errors: list[dict[str, Any]] = []

    validator = Draft202012Validator(schema)
    for error in sorted(validator.iter_errors(context), key=lambda item: list(item.absolute_path)):
        path_tokens = tuple(error.absolute_path)
        errors.append(
            {
                "path": _format_json_pointer(path_tokens),
                "line": _line_for_path(path_tokens, line_map),
                "message": error.message,
            }
        )

    errors.extend(_normative_contract_checks(context, line_map))

    return ValidationReport(
        valid=not errors,
        errors=errors,
        warnings=[],
        context=context,
    )


def _normative_contract_checks(
    context: dict[str, Any],
    line_map: dict[tuple[Any, ...], int],
) -> list[dict[str, Any]]:
    errors: list[dict[str, Any]] = []

    for key in REQUIRED_ROOT_FIELDS:
        if key not in context:
            errors.append(
                {
                    "path": f"/{key}",
                    "line": _line_for_path((key,), line_map),
                    "message": f"Missing required root field: {key}",
                }
            )

    if context.get("version") not in {"0.1", "0.2"}:
        errors.append(
            {
                "path": "/version",
                "line": _line_for_path(("version",), line_map),
                "message": 'version MUST be one of: "0.1", "0.2"',
            }
        )

    _validate_unique_ids(
        section_name="trust_boundaries",
        items=context.get("trust_boundaries"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="data_classifications",
        items=context.get("data_classifications"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="approved_patterns",
        items=context.get("approved_patterns"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="anti_patterns",
        items=context.get("anti_patterns"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="authorization_policies",
        items=context.get("authorization_policies"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="input_validations",
        items=context.get("input_validations"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="security_configs",
        items=context.get("security_configs"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="secrets_management",
        items=context.get("secrets_management"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="pipeline_policies",
        items=context.get("pipeline_policies"),
        line_map=line_map,
        errors=errors,
    )
    _validate_unique_ids(
        section_name="error_handling",
        items=context.get("error_handling"),
        line_map=line_map,
        errors=errors,
    )

    for section_name, rules_key in [
        ("trust_boundaries", "crossing_requirements"),
        ("data_classifications", "handling_rules"),
        ("approved_patterns", "requirements"),
        ("anti_patterns", "requirements"),
        ("authorization_policies", "required_controls"),
        ("input_validations", "validation_rules"),
        ("security_configs", "rules"),
        ("secrets_management", "rules"),
        ("pipeline_policies", "rules"),
        ("error_handling", "rules"),
    ]:
        section = context.get(section_name)
        if not isinstance(section, list):
            continue

        for idx, item in enumerate(section):
            if not isinstance(item, dict):
                continue
            rules = item.get(rules_key)
            if rules is None:
                continue
            _validate_unique_requirement_ids(
                owner_path=(section_name, idx, rules_key),
                rules=rules,
                line_map=line_map,
                errors=errors,
            )

    return errors


def _validate_unique_ids(
    section_name: str,
    items: Any,
    line_map: dict[tuple[Any, ...], int],
    errors: list[dict[str, Any]],
) -> None:
    if not isinstance(items, list):
        return

    seen: dict[str, int] = {}
    for idx, item in enumerate(items):
        if not isinstance(item, dict):
            continue

        item_id = item.get("id")
        path = (section_name, idx, "id")
        if not isinstance(item_id, str):
            continue

        if not ID_PATTERN.fullmatch(item_id):
            errors.append(
                {
                    "path": _format_json_pointer(path),
                    "line": _line_for_path(path, line_map),
                    "message": "ID must be kebab-case.",
                }
            )

        previous_idx = seen.get(item_id)
        if previous_idx is not None:
            errors.append(
                {
                    "path": _format_json_pointer(path),
                    "line": _line_for_path(path, line_map),
                    "message": f'Duplicate id "{item_id}" in {section_name}.',
                }
            )
        else:
            seen[item_id] = idx


def _validate_unique_requirement_ids(
    owner_path: tuple[Any, ...],
    rules: Any,
    line_map: dict[tuple[Any, ...], int],
    errors: list[dict[str, Any]],
) -> None:
    if not isinstance(rules, list):
        return

    seen: dict[str, tuple[int, dict[str, Any]]] = {}
    for idx, rule in enumerate(rules):
        if not isinstance(rule, dict):
            continue

        rule_id = rule.get("id")
        if not isinstance(rule_id, str):
            continue

        current_path = owner_path + (idx, "id")
        previous = seen.get(rule_id)
        if previous is None:
            seen[rule_id] = (idx, rule)
            continue

        previous_idx, previous_rule = previous
        if previous_rule != rule:
            errors.append(
                {
                    "path": _format_json_pointer(current_path),
                    "line": _line_for_path(current_path, line_map),
                    "message": (
                        f'Conflicting duplicate requirement id "{rule_id}" '
                        f"inside {_format_json_pointer(owner_path)}."
                    ),
                }
            )
        else:
            errors.append(
                {
                    "path": _format_json_pointer(current_path),
                    "line": _line_for_path(current_path, line_map),
                    "message": (
                        f'Duplicate requirement id "{rule_id}" '
                        f"inside {_format_json_pointer(owner_path)} "
                        f"(first occurrence at index {previous_idx})."
                    ),
                }
            )


def _build_line_map(raw_text: str) -> dict[tuple[Any, ...], int]:
    try:
        node = yaml.compose(raw_text)
    except yaml.YAMLError:
        return {}

    if node is None:
        return {}

    line_map: dict[tuple[Any, ...], int] = {}

    def walk(current: Any, path: tuple[Any, ...]) -> None:
        line_map[path] = current.start_mark.line + 1

        if isinstance(current, MappingNode):
            for key_node, value_node in current.value:
                key = key_node.value
                key_path = path + (key,)
                line_map[key_path] = key_node.start_mark.line + 1
                walk(value_node, key_path)
        elif isinstance(current, SequenceNode):
            for idx, child in enumerate(current.value):
                child_path = path + (idx,)
                line_map[child_path] = child.start_mark.line + 1
                walk(child, child_path)

    walk(node, tuple())
    return line_map


def _line_for_path(
    path_tokens: Iterable[Any],
    line_map: dict[tuple[Any, ...], int],
) -> int | None:
    path = tuple(path_tokens)
    while path:
        if path in line_map:
            return line_map[path]
        path = path[:-1]
    return line_map.get(tuple())


def _format_json_pointer(path_tokens: Iterable[Any]) -> str:
    path = list(path_tokens)
    if not path:
        return "/"

    def escape(token: Any) -> str:
        token_str = str(token)
        return token_str.replace("~", "~0").replace("/", "~1")

    return "/" + "/".join(escape(token) for token in path)
