# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Policy loading from various sources."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any

from mergeguide.engine.rules import Rule, RuleType, Severity

from .schema import PolicyFile, PolicySchema

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)


class PolicyLoadError(Exception):
    """Error loading policies."""

    pass


class PolicyLoader:
    """Load policies from various sources.

    Supports loading from:
    - Local YAML files
    - Directories of YAML files
    - DynamoDB (via AWS integration)
    """

    def __init__(self) -> None:
        self.schema = PolicySchema()
        self._cached_policies: dict[str, PolicyFile] = {}

    def load_file(self, file_path: str | Path) -> PolicyFile:
        """Load a policy file from disk.

        Args:
            file_path: Path to the YAML file

        Returns:
            Parsed PolicyFile

        Raises:
            PolicyLoadError: If file cannot be loaded or is invalid
        """
        path = Path(file_path)
        if not path.exists():
            raise PolicyLoadError(f"Policy file not found: {path}")

        try:
            content = path.read_text()
        except OSError as e:
            raise PolicyLoadError(f"Cannot read policy file: {e}") from e

        errors = self.schema.validate_yaml(content)
        if errors:
            raise PolicyLoadError(
                f"Invalid policy file {path}:\n" + "\n".join(f"  - {e}" for e in errors)
            )

        return PolicyFile.from_yaml(content)

    def load_directory(self, dir_path: str | Path) -> list[PolicyFile]:
        """Load all policy files from a directory.

        Args:
            dir_path: Path to directory containing policy YAML files

        Returns:
            List of parsed PolicyFiles

        Raises:
            PolicyLoadError: If directory cannot be read
        """
        path = Path(dir_path)
        if not path.is_dir():
            raise PolicyLoadError(f"Not a directory: {path}")

        policy_files = []
        for yaml_file in path.glob("*.yaml"):
            try:
                policy_files.append(self.load_file(yaml_file))
            except PolicyLoadError as e:
                # Log warning but continue loading other files
                import logging

                logging.warning(f"Skipping invalid policy file: {e}")
                continue

        for yml_file in path.glob("*.yml"):
            try:
                policy_files.append(self.load_file(yml_file))
            except PolicyLoadError as e:
                import logging

                logging.warning(f"Skipping invalid policy file: {e}")
                continue

        return policy_files

    def load_from_dict(self, data: dict[str, Any]) -> PolicyFile:
        """Load a policy from a dictionary.

        Args:
            data: Policy data dictionary

        Returns:
            Parsed PolicyFile

        Raises:
            PolicyLoadError: If data is invalid
        """
        errors = self.schema.validate(data)
        if errors:
            raise PolicyLoadError("Invalid policy data:\n" + "\n".join(f"  - {e}" for e in errors))

        return PolicyFile.from_dict(data)

    def load_rules(self, org_id: str = "default") -> list[Rule]:
        """Load rules for an organization.

        This is a convenience method that loads all policies
        and converts them to Rule objects for the engine.

        Args:
            org_id: Organization ID (for future multi-tenant support)

        Returns:
            List of Rule objects
        """
        rules: list[Rule] = []

        # Check for policies in standard locations
        policy_paths = [
            Path(".mergeguide/policies"),
            Path("policies"),
            Path.home() / ".mergeguide" / "policies",
        ]

        for policy_path in policy_paths:
            if policy_path.is_dir():
                policy_files = self.load_directory(policy_path)
                for policy_file in policy_files:
                    rules.extend(self._policy_file_to_rules(policy_file))

        # Also check for single policy file
        single_policy_paths = [
            Path(".mergeguide.yaml"),
            Path(".mergeguide.yml"),
            Path("mergeguide.yaml"),
            Path("mergeguide.yml"),
        ]

        for policy_path in single_policy_paths:
            if policy_path.exists():
                try:
                    policy_file = self.load_file(policy_path)
                    rules.extend(self._policy_file_to_rules(policy_file))
                except PolicyLoadError:
                    continue

        return rules

    def _policy_file_to_rules(self, policy_file: PolicyFile) -> list[Rule]:
        """Convert a PolicyFile to a list of Rule objects."""
        rules: list[Rule] = []

        for policy in policy_file.policies:
            if not policy.enabled:
                continue

            for i, rule_def in enumerate(policy.rules):
                rule_id = f"{policy.id}-{i}" if len(policy.rules) > 1 else policy.id
                # Use rule-level severity if specified, otherwise fall back to policy severity
                rule_severity = rule_def.severity if rule_def.severity else policy.severity
                rules.append(
                    Rule(
                        id=rule_id,
                        name=policy.name,
                        pattern=rule_def.pattern,
                        message=rule_def.message,
                        severity=Severity.from_string(rule_severity),
                        rule_type=RuleType.REGEX,
                        languages=policy.languages,
                        remediation=rule_def.remediation,
                        flags=rule_def.flags,
                    )
                )

        return rules


class DynamoDBPolicyLoader(PolicyLoader):
    """Load policies from DynamoDB with TTL caching.

    This loader fetches policies from DynamoDB and caches them in memory
    to reduce database load. The cache is automatically invalidated when
    policies are saved.

    Table schema:
    - pk: ORG#{org_id}
    - sk: POLICY#{policy_id}
    - policy_data: YAML policy content

    Cache behavior:
    - Policies are cached for 5 minutes (configurable via MERGEGUIDE_POLICY_CACHE_TTL)
    - Cache is invalidated on save_policy()
    - Use bypass_cache=True or set MERGEGUIDE_CACHE_DISABLED=true to bypass
    """

    def __init__(self, table_name: str, region: str = "us-east-1") -> None:
        super().__init__()
        self.table_name = table_name
        self.region = region
        self._dynamodb = None

    @property
    def dynamodb(self) -> Any:
        """Lazy-load DynamoDB resource."""
        if self._dynamodb is None:
            import boto3

            self._dynamodb = boto3.resource("dynamodb", region_name=self.region)
        return self._dynamodb

    @property
    def table(self) -> Any:
        """Get the DynamoDB table."""
        return self.dynamodb.Table(self.table_name)

    def load_rules(
        self,
        org_id: str = "default",
        repo_id: str | None = None,
        bypass_cache: bool = False,
    ) -> list[Rule]:
        """Load rules for an organization with optional repo-specific overrides.

        Policy inheritance model:
        1. Load org-level policies
        2. If repo_id is specified, apply repo-level overrides

        Args:
            org_id: Organization ID to load policies for.
            repo_id: Optional repository ID (format: owner/repo) for repo-specific rules.
            bypass_cache: If True, skip cache and load directly from DynamoDB.

        Returns:
            List of Rule objects for the organization/repository.
        """
        from mergeguide.cache import get_policy_cache, is_cache_disabled

        # Include repo in cache key if specified
        cache_key = f"policy:{org_id}:{repo_id}" if repo_id else f"policy:{org_id}"

        # Check cache first (unless bypassed or disabled)
        if not bypass_cache and not is_cache_disabled():
            cache = get_policy_cache()
            cached_rules = cache.get(cache_key)
            if cached_rules is not None:
                logger.debug(f"Cache hit for {cache_key} ({len(cached_rules)} rules)")
                return cached_rules

        # Load from DynamoDB with optional repo overrides
        rules = self._load_from_dynamodb(org_id, repo_id)

        # Also load local policies as fallback/override
        local_rules = super().load_rules(org_id)
        rules.extend(local_rules)

        # Cache the results (unless disabled)
        if not is_cache_disabled():
            cache = get_policy_cache()
            cache.set(cache_key, rules)
            logger.debug(f"Cached {len(rules)} rules for {cache_key}")

        return rules

    def _load_from_dynamodb(self, org_id: str, repo_id: str | None = None) -> list[Rule]:
        """Load rules from DynamoDB with optional repo overrides.

        Args:
            org_id: Organization ID to load policies for.
            repo_id: Optional repository ID for repo-specific overrides.

        Returns:
            List of Rule objects from DynamoDB.
        """
        # Load org policies
        org_policies = self._load_org_policies(org_id)

        # Apply repo overrides if specified
        if repo_id:
            overrides = self._load_repo_overrides(org_id, repo_id)
            return self._merge_policies_with_overrides(org_policies, overrides)

        return self._policies_to_rules(org_policies)

    def _load_org_policies(self, org_id: str) -> list[dict[str, Any]]:
        """Load org-level policies from DynamoDB."""
        policies: list[dict[str, Any]] = []

        try:
            response = self.table.query(
                KeyConditionExpression="pk = :pk AND begins_with(sk, :sk_prefix)",
                ExpressionAttributeValues={
                    ":pk": f"ORG#{org_id}",
                    ":sk_prefix": "POLICY#",
                },
            )

            for item in response.get("Items", []):
                policy_yaml = item.get("policy_data", "")
                if policy_yaml:
                    try:
                        policy_file = PolicyFile.from_yaml(policy_yaml)
                        for policy in policy_file.policies:
                            policies.append(
                                {
                                    "policy_id": item.get("policy_id", policy.id),
                                    "name": policy.name,
                                    "severity": policy.severity,
                                    "languages": policy.languages,
                                    "enabled": policy.enabled,
                                    "rules": [
                                        {
                                            "id": f"{policy.id}-{i}"
                                            if len(policy.rules) > 1
                                            else policy.id,
                                            "pattern": r.pattern,
                                            "message": r.message,
                                            "remediation": r.remediation,
                                            "flags": r.flags,
                                        }
                                        for i, r in enumerate(policy.rules)
                                    ],
                                }
                            )
                    except Exception as e:
                        logger.debug(f"Failed to parse policy: {e}")
                        continue
                else:
                    # Structured policy data from dashboard
                    policies.append(
                        {
                            "policy_id": item.get("policy_id"),
                            "name": item.get("name"),
                            "severity": item.get("severity", "warning"),
                            "languages": item.get("languages", ["*"]),
                            "enabled": item.get("enabled", True),
                            "rules": item.get("rules", []),
                        }
                    )

        except Exception as e:
            logger.warning(f"Failed to load policies from DynamoDB: {e}")

        return policies

    def _load_repo_overrides(self, org_id: str, repo_id: str) -> dict[str, dict[str, Any]]:
        """Load repository-level policy overrides from DynamoDB."""
        overrides: dict[str, dict[str, Any]] = {}

        try:
            response = self.table.query(
                KeyConditionExpression="pk = :pk AND begins_with(sk, :sk_prefix)",
                ExpressionAttributeValues={
                    ":pk": f"ORG#{org_id}#REPO#{repo_id}",
                    ":sk_prefix": "OVERRIDE#",
                },
            )

            for item in response.get("Items", []):
                policy_id = item.get("policy_id")
                if policy_id:
                    overrides[policy_id] = {
                        "enabled": item.get("enabled"),
                        "severity": item.get("severity"),
                        "additional_rules": item.get("additional_rules", []),
                        "excluded_rule_ids": item.get("excluded_rule_ids", []),
                    }

        except Exception as e:
            logger.warning(f"Failed to load repo overrides: {e}")

        return overrides

    def _merge_policies_with_overrides(
        self,
        policies: list[dict[str, Any]],
        overrides: dict[str, dict[str, Any]],
    ) -> list[Rule]:
        """Merge org policies with repo overrides and convert to rules."""
        rules: list[Rule] = []

        for policy in policies:
            policy_id = policy.get("policy_id")
            override = overrides.get(policy_id, {}) if policy_id else {}

            # Apply enabled override
            enabled = override.get("enabled")
            if enabled is None:
                enabled = policy.get("enabled", True)
            if not enabled:
                continue

            # Apply severity override
            severity_str = override.get("severity")
            if severity_str is None:
                severity_str = policy.get("severity", "warning")
            severity = Severity.from_string(severity_str)

            languages = policy.get("languages", ["*"])
            policy_rules = list(policy.get("rules", []))
            excluded_ids = set(override.get("excluded_rule_ids", []))

            # Filter out excluded rules
            policy_rules = [r for r in policy_rules if r.get("id", "") not in excluded_ids]

            # Add additional rules from override
            additional_rules = override.get("additional_rules", [])
            policy_rules.extend(additional_rules)

            for rule_def in policy_rules:
                rule_id = rule_def.get("id", policy.get("name", "rule"))
                # Use rule-level severity if specified, otherwise use policy/override severity
                rule_severity_str = rule_def.get("severity")
                rule_severity = (
                    Severity.from_string(rule_severity_str) if rule_severity_str else severity
                )
                rules.append(
                    Rule(
                        id=rule_id,
                        name=policy.get("name", ""),
                        pattern=rule_def.get("pattern", ""),
                        message=rule_def.get("message", ""),
                        severity=rule_severity,
                        rule_type=RuleType.REGEX,
                        languages=languages,
                        remediation=rule_def.get("remediation"),
                        flags=rule_def.get("flags"),
                    )
                )

        return rules

    def _policies_to_rules(self, policies: list[dict[str, Any]]) -> list[Rule]:
        """Convert policy dictionaries to Rule objects (no overrides)."""
        return self._merge_policies_with_overrides(policies, {})

    def save_policy(self, org_id: str, policy_id: str, policy_yaml: str) -> None:
        """Save a policy to DynamoDB and invalidate cache.

        Args:
            org_id: Organization ID
            policy_id: Policy identifier
            policy_yaml: YAML policy content
        """
        # Validate before saving
        errors = self.schema.validate_yaml(policy_yaml)
        if errors:
            raise PolicyLoadError("Invalid policy:\n" + "\n".join(f"  - {e}" for e in errors))

        self.table.put_item(
            Item={
                "pk": f"ORG#{org_id}",
                "sk": f"POLICY#{policy_id}",
                "policy_data": policy_yaml,
            }
        )

        # Invalidate cache for this organization
        from mergeguide.cache import invalidate_org

        invalidate_org(org_id)
        logger.info(f"Saved and invalidated cache for policy {policy_id} in org {org_id}")
