"""
Policy Parser
=============

YAML parser for HIEROMEM memory policies.
# Phase 1 verified - flake8 clean

Supports loading policies from:
- File paths
- YAML strings
- Dictionaries

Validates policies against the schema and provides helpful error messages.
"""

from __future__ import annotations

import os
from datetime import timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

import yaml

from hieromem_core.policy.schema import (
    DecayCurve,
    GovernancePolicy,
    GraphPolicy,
    MemoryPolicy,
    PriorityPolicy,
    RetentionPolicy,
    SummarizationPolicy,
)


class PolicyParseError(Exception):
    """Raised when policy parsing fails."""

    def __init__(self, message: str, path: Optional[str] = None, line: Optional[int] = None):
        self.path = path
        self.line = line
        location = ""
        if path:
            location = f" in {path}"
        if line:
            location += f" at line {line}"
        super().__init__(f"Policy parse error{location}: {message}")


class PolicyValidationError(Exception):
    """Raised when policy validation fails."""

    def __init__(self, message: str, field: Optional[str] = None):
        self.field = field
        field_info = f" (field: {field})" if field else ""
        super().__init__(f"Policy validation error{field_info}: {message}")


class PolicyParser:
    """
    Parser for HIEROMEM memory policies in YAML format.

    Example YAML format:
    ```yaml
    apiVersion: hieromem/v1
    kind: MemoryPolicy
    metadata:
      name: my-policy
      namespace: default
      description: Custom memory policy
    spec:
      enabled: true
      retention:
        horizon: 30  # days
        decay_curve: exponential
        base_lambda: 0.95
        min_trust: 0.1
      summarization:
        max_episodes: 100
        depth: 2
      graph:
        edge_ttl: 60  # days
        max_hops: 5
      governance:
        forbidden_relations:
          - kills
          - harms
        max_episodes: 10000
    ```
    """

    # Supported API versions
    SUPPORTED_VERSIONS = {"hieromem/v1"}

    def __init__(self, strict: bool = True):
        """
        Initialize the parser.

        Args:
            strict: If True, fail on unknown fields. If False, ignore them.
        """
        self.strict = strict

    def parse_file(self, path: Union[str, Path]) -> MemoryPolicy:
        """
        Parse a policy from a YAML file.

        Args:
            path: Path to the YAML file.

        Returns:
            Parsed MemoryPolicy object.

        Raises:
            PolicyParseError: If file cannot be read or parsed.
            PolicyValidationError: If policy is invalid.
        """
        path = Path(path)
        if not path.exists():
            raise PolicyParseError(f"File not found: {path}")

        try:
            with open(path, "r") as f:
                content = f.read()
        except IOError as e:
            raise PolicyParseError(f"Cannot read file: {e}", path=str(path))

        return self.parse_string(content, source=str(path))

    def parse_string(self, content: str, source: Optional[str] = None) -> MemoryPolicy:
        """
        Parse a policy from a YAML string.

        Args:
            content: YAML content as string.
            source: Optional source identifier for error messages.

        Returns:
            Parsed MemoryPolicy object.

        Raises:
            PolicyParseError: If YAML is invalid.
            PolicyValidationError: If policy is invalid.
        """
        try:
            data = yaml.safe_load(content)
        except yaml.YAMLError as e:
            raise PolicyParseError(f"Invalid YAML: {e}", path=source)

        if data is None:
            raise PolicyParseError("Empty YAML document", path=source)

        return self.parse_dict(data, source=source)

    def parse_dict(self, data: Dict[str, Any], source: Optional[str] = None) -> MemoryPolicy:
        """
        Parse a policy from a dictionary.

        Args:
            data: Policy dictionary.
            source: Optional source identifier for error messages.

        Returns:
            Parsed MemoryPolicy object.

        Raises:
            PolicyValidationError: If policy is invalid.
        """
        # Validate structure
        self._validate_structure(data, source)

        # Extract metadata
        metadata = data.get("metadata", {})
        spec = data.get("spec", {})

        # Parse name (required)
        name = metadata.get("name")
        if not name:
            raise PolicyValidationError("Policy name is required", field="metadata.name")

        # Parse sub-policies
        retention = self._parse_retention(spec.get("retention", {}))
        summarization = self._parse_summarization(spec.get("summarization", {}))
        graph = self._parse_graph(spec.get("graph", {}))
        governance = self._parse_governance(spec.get("governance", {}))
        priority = self._parse_priority(spec.get("priority", {}))

        return MemoryPolicy(
            name=name,
            namespace=metadata.get("namespace", "*"),
            version=metadata.get("version", "1.0"),
            description=metadata.get("description", ""),
            enabled=spec.get("enabled", True),
            retention=retention,
            summarization=summarization,
            graph=graph,
            governance=governance,
            priority=priority,
        )

    def _validate_structure(self, data: Dict[str, Any], source: Optional[str] = None) -> None:
        """Validate the top-level policy structure."""
        # Check apiVersion
        api_version = data.get("apiVersion")
        if not api_version:
            raise PolicyValidationError("apiVersion is required", field="apiVersion")
        if api_version not in self.SUPPORTED_VERSIONS:
            raise PolicyValidationError(
                f"Unsupported apiVersion: {api_version}. Supported: {self.SUPPORTED_VERSIONS}",
                field="apiVersion",
            )

        # Check kind
        kind = data.get("kind")
        if kind != "MemoryPolicy":
            raise PolicyValidationError(
                f"Invalid kind: {kind}. Expected: MemoryPolicy",
                field="kind",
            )

        # Check required sections
        if "metadata" not in data:
            raise PolicyValidationError("metadata section is required", field="metadata")
        if "spec" not in data:
            raise PolicyValidationError("spec section is required", field="spec")

    def _parse_duration(self, value: Any) -> Optional[timedelta]:
        """
        Parse a duration value into a timedelta.

        Supports:
        - None -> None
        - timedelta -> timedelta
        - int/float -> timedelta(days=value)
        - str -> parsed as duration (e.g. "30d", "12h")
        """
        if value is None:
            return None

        if isinstance(value, timedelta):
            return value

        if isinstance(value, (int, float)):
            return timedelta(days=float(value))

        if isinstance(value, str):
            value = value.strip().lower()
            if not value:
                return None

            try:
                if value.endswith("d"):
                    return timedelta(days=float(value[:-1]))
                elif value.endswith("h"):
                    return timedelta(hours=float(value[:-1]))
                elif value.endswith("m"):
                    return timedelta(minutes=float(value[:-1]))
                elif value.endswith("s"):
                    return timedelta(seconds=float(value[:-1]))
                elif value.endswith("w"):
                    return timedelta(weeks=float(value[:-1]))
                else:
                    # Try parsing as plain number (assume days for backward compat)
                    return timedelta(days=float(value))
            except ValueError:
                pass

        # If we get here, it's an invalid format
        raise ValueError(f"Invalid duration format: {value}")

    def _parse_retention(self, data: Dict[str, Any]) -> RetentionPolicy:
        """Parse retention policy section."""
        kwargs = {}

        if "horizon" in data:
            try:
                kwargs["horizon"] = self._parse_duration(data["horizon"])
            except ValueError as e:
                raise PolicyValidationError(str(e), field="spec.retention.horizon")

        if "decay_curve" in data:
            curve = data["decay_curve"]
            if isinstance(curve, str):
                try:
                    kwargs["decay_curve"] = DecayCurve(curve.lower())
                except ValueError:
                    valid = [c.value for c in DecayCurve]
                    raise PolicyValidationError(
                        f"Invalid decay_curve: {curve}. Valid: {valid}",
                        field="spec.retention.decay_curve",
                    )

        for field in ["base_lambda", "min_trust", "access_boost"]:
            if field in data:
                value = data[field]
                if not isinstance(value, (int, float)):
                    raise PolicyValidationError(
                        f"{field} must be a number",
                        field=f"spec.retention.{field}",
                    )
                if field == "base_lambda" and not (0 <= value <= 1):
                    raise PolicyValidationError(
                        f"base_lambda must be between 0 and 1, got {value}",
                        field="spec.retention.base_lambda",
                    )
                kwargs[field] = float(value)

        if "decay_interval" in data:
            kwargs["decay_interval"] = int(data["decay_interval"]) if data["decay_interval"] else None

        return RetentionPolicy(**kwargs)

    def _parse_summarization(self, data: Dict[str, Any]) -> SummarizationPolicy:
        """Parse summarization policy section."""
        kwargs = {}

        if "max_episodes" in data:
            kwargs["max_episodes"] = int(data["max_episodes"]) if data["max_episodes"] else None

        if "max_age" in data:
            try:
                kwargs["max_age"] = self._parse_duration(data["max_age"])
            except ValueError as e:
                raise PolicyValidationError(str(e), field="spec.summarization.max_age")

        if "depth" in data:
            kwargs["depth"] = int(data["depth"])

        if "preserve_graph" in data:
            kwargs["preserve_graph"] = bool(data["preserve_graph"])

        if "min_fidelity" in data:
            value = data["min_fidelity"]
            if not (0 <= value <= 1):
                raise PolicyValidationError(
                    f"min_fidelity must be between 0 and 1, got {value}",
                    field="spec.summarization.min_fidelity",
                )
            kwargs["min_fidelity"] = float(value)

        if "batch_size" in data:
            kwargs["batch_size"] = int(data["batch_size"])

        return SummarizationPolicy(**kwargs)

    def _parse_graph(self, data: Dict[str, Any]) -> GraphPolicy:
        """Parse graph policy section."""
        kwargs = {}

        if "edge_ttl" in data:
            try:
                kwargs["edge_ttl"] = self._parse_duration(data["edge_ttl"])
            except ValueError as e:
                raise PolicyValidationError(str(e), field="spec.graph.edge_ttl")

        if "prune_orphans" in data:
            kwargs["prune_orphans"] = bool(data["prune_orphans"])

        if "max_hops" in data:
            value = data["max_hops"]
            if value < 1:
                raise PolicyValidationError(
                    f"max_hops must be at least 1, got {value}",
                    field="spec.graph.max_hops",
                )
            kwargs["max_hops"] = int(value)

        if "max_edges_per_node" in data:
            kwargs["max_edges_per_node"] = int(data["max_edges_per_node"]) if data["max_edges_per_node"] else None

        if "protected_edge_types" in data:
            kwargs["protected_edge_types"] = set(data["protected_edge_types"])

        return GraphPolicy(**kwargs)

    def _parse_governance(self, data: Dict[str, Any]) -> GovernancePolicy:
        """Parse governance policy section."""
        kwargs = {}

        if "forbidden_relations" in data:
            kwargs["forbidden_relations"] = set(data["forbidden_relations"])

        if "max_episodes" in data:
            kwargs["max_episodes"] = int(data["max_episodes"]) if data["max_episodes"] else None

        for field in ["max_trust_boost", "min_lambda", "max_lambda"]:
            if field in data:
                kwargs[field] = float(data[field])

        if "audit_required" in data:
            kwargs["audit_required"] = bool(data["audit_required"])

        if "blocked_content_patterns" in data:
            patterns = data["blocked_content_patterns"]
            if not isinstance(patterns, list):
                raise PolicyValidationError(
                    "blocked_content_patterns must be a list",
                    field="spec.governance.blocked_content_patterns",
                )
            kwargs["blocked_content_patterns"] = patterns

        return GovernancePolicy(**kwargs)

    def _parse_priority(self, data: Dict[str, Any]) -> PriorityPolicy:
        """Parse priority policy section."""
        kwargs = {}

        for field in ["high_priority_patterns", "low_priority_patterns"]:
            if field in data:
                patterns = data[field]
                if not isinstance(patterns, list):
                    raise PolicyValidationError(
                        f"{field} must be a list",
                        field=f"spec.priority.{field}",
                    )
                kwargs[field] = patterns

        for field in ["high_priority_lambda_multiplier", "low_priority_lambda_multiplier"]:
            if field in data:
                kwargs[field] = float(data[field])

        if "critical_tags" in data:
            kwargs["critical_tags"] = set(data["critical_tags"])

        return PriorityPolicy(**kwargs)

    def load_directory(self, path: Union[str, Path]) -> List[MemoryPolicy]:
        """
        Load all policy files from a directory.

        Args:
            path: Directory path containing .yaml/.yml files.

        Returns:
            List of parsed MemoryPolicy objects.

        Raises:
            PolicyParseError: If directory doesn't exist.
        """
        path = Path(path)
        if not path.is_dir():
            raise PolicyParseError(f"Not a directory: {path}")

        policies = []
        for file_path in sorted(path.glob("*.yaml")) + sorted(path.glob("*.yml")):
            try:
                policy = self.parse_file(file_path)
                policies.append(policy)
            except (PolicyParseError, PolicyValidationError) as e:
                # Re-raise with file context
                raise PolicyParseError(f"Error loading {file_path}: {e}")

        return policies


def load_policy(source: Union[str, Path, Dict[str, Any]]) -> MemoryPolicy:
    """
    Convenience function to load a policy from any source.

    Args:
        source: File path, YAML string, or dictionary.

    Returns:
        Parsed MemoryPolicy object.
    """
    parser = PolicyParser()

    if isinstance(source, dict):
        return parser.parse_dict(source)
    elif isinstance(source, Path) or (isinstance(source, str) and os.path.exists(source)):
        return parser.parse_file(source)
    elif isinstance(source, str):
        return parser.parse_string(source)
    else:
        raise PolicyParseError(f"Invalid source type: {type(source)}")
