"""YAML policy file loader with schema version validation.

Loads roles and policies from YAML files. Supports single files or directories.
Validates that each file has a 'version' field matching the supported schema.
"""

from pathlib import Path

import yaml

from pyauthz.core.errors import ConfigError, PolicyVersionError, SchemaError
from pyauthz.models.action import Effect
from pyauthz.models.condition import Condition
from pyauthz.models.policy import Policy
from pyauthz.models.role import Role

SUPPORTED_VERSIONS: set[int] = {1}


def load_yaml_policies(path: str) -> tuple[list[Role], list[Policy]]:
    """Load roles and policies from a YAML file or directory.

    If path points to a directory, all .yaml and .yml files in it are loaded.
    If path points to a single file, that file is loaded.

    Args:
        path: Path to a YAML file or directory containing YAML files.

    Returns:
        A tuple of (roles, policies) parsed from the YAML files.

    Raises:
        SchemaError: If a file has invalid structure.
        PolicyVersionError: If a file has an unsupported or missing version.
        ConfigError: If the path doesn't exist.
    """
    p = Path(path)
    if not p.exists():
        raise ConfigError(f"Path does not exist: {path}")

    if p.is_dir():
        files = sorted(p.glob("*.yaml")) + sorted(p.glob("*.yml"))
        if not files:
            raise ConfigError(f"No YAML files found in: {path}")
    else:
        files = [p]

    all_roles: list[Role] = []
    all_policies: list[Policy] = []

    for file_path in files:
        roles, policies = _load_single_file(file_path)
        all_roles.extend(roles)
        all_policies.extend(policies)

    return all_roles, all_policies


def _load_single_file(file_path: Path) -> tuple[list[Role], list[Policy]]:
    """Load roles and policies from a single YAML file."""
    file_str = str(file_path)

    try:
        with open(file_path) as f:
            data = yaml.safe_load(f)
    except yaml.YAMLError as e:
        raise SchemaError(file_str, f"Invalid YAML: {e}") from e

    if not isinstance(data, dict):
        raise SchemaError(file_str, "Expected a YAML mapping at the top level")

    # Validate version
    version = data.get("version")
    if version is None:
        raise PolicyVersionError(file_str, None, SUPPORTED_VERSIONS)
    if not isinstance(version, int):
        raise SchemaError(file_str, f"'version' must be an integer, got {type(version).__name__}")
    if version not in SUPPORTED_VERSIONS:
        raise PolicyVersionError(file_str, version, SUPPORTED_VERSIONS)

    roles = _parse_roles(data.get("roles", []), file_str)
    policies = _parse_policies(data.get("policies", []), file_str)

    return roles, policies


def _parse_roles(raw_roles: list, file_str: str) -> list[Role]:
    """Parse role definitions from YAML data."""
    if not isinstance(raw_roles, list):
        raise SchemaError(file_str, "'roles' must be a list")

    roles: list[Role] = []
    for i, raw in enumerate(raw_roles):
        if not isinstance(raw, dict):
            raise SchemaError(file_str, f"roles[{i}]: expected a mapping")
        if "name" not in raw:
            raise SchemaError(file_str, f"roles[{i}]: missing required field 'name'")

        roles.append(
            Role(
                name=raw["name"],
                permissions=[str(p) for p in raw.get("permissions", [])],
                parents=[str(p) for p in raw.get("parents", [])],
            )
        )

    return roles


def _parse_policies(raw_policies: list, file_str: str) -> list[Policy]:
    """Parse policy definitions from YAML data."""
    if not isinstance(raw_policies, list):
        raise SchemaError(file_str, "'policies' must be a list")

    policies: list[Policy] = []
    for i, raw in enumerate(raw_policies):
        if not isinstance(raw, dict):
            raise SchemaError(file_str, f"policies[{i}]: expected a mapping")

        required = ["name", "effect"]
        for field in required:
            if field not in raw:
                raise SchemaError(file_str, f"policies[{i}]: missing required field '{field}'")

        # Parse effect
        effect_str = raw["effect"].lower()
        try:
            effect = Effect(effect_str)
        except ValueError as e:
            raise SchemaError(
                file_str,
                f"policies[{i}]: invalid effect '{raw['effect']}', must be 'allow' or 'deny'",
            ) from e

        # Parse conditions
        conditions: list[Condition] = []
        for j, cond_raw in enumerate(raw.get("conditions", [])):
            if not isinstance(cond_raw, dict):
                raise SchemaError(file_str, f"policies[{i}].conditions[{j}]: expected a mapping")
            for field in ["field", "op", "value"]:
                if field not in cond_raw:
                    raise SchemaError(
                        file_str,
                        f"policies[{i}].conditions[{j}]: missing required field '{field}'",
                    )
            conditions.append(
                Condition(
                    field=cond_raw["field"],
                    op=cond_raw["op"],
                    value=cond_raw["value"],
                )
            )

        policies.append(
            Policy(
                name=raw["name"],
                description=raw.get("description", ""),
                effect=effect,
                roles=[str(r) for r in raw.get("roles", [])],
                permissions=[str(p) for p in raw.get("permissions", [])],
                conditions=conditions,
                priority=raw.get("priority", 0),
                enabled=raw.get("enabled", True),
            )
        )

    return policies
