"""
Smart JSON merger for settings.json with intelligent permission handling.
"""

import json
from pathlib import Path
from typing import Any


class SettingsMerger:
    """
    Intelligent JSON merger for settings.json.

    Union Mode (default):
        - permissions.allow: Union of both sets (deduplicated with wildcard awareness)
        - permissions.deny: Target preserved (security boundary)
        - hooks: Merge by event+matcher, preserve all

    Strict Mode:
        - permissions.allow: Source only (removes custom perms)
        - permissions.deny: Target preserved (never modify)
        - hooks: Source only
    """

    def merge(
        self, source: dict[str, Any], target: dict[str, Any] | None, mode: str = "union"
    ) -> dict[str, Any]:
        """
        Merge settings with specified strategy.

        Args:
            source: Template settings from source repo
            target: Existing target project settings (may be None)
            mode: 'union' or 'strict'

        Returns:
            Merged settings dictionary
        """
        if target is None:
            return source

        if mode == "strict":
            return self._strict_merge(source, target)
        else:
            return self._union_merge(source, target)

    def _union_merge(self, source: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]:
        """Union merge strategy - preserves custom permissions."""
        result: dict[str, Any] = {}

        # Merge permissions
        result["permissions"] = {}

        # Allow list: union with deduplication
        result["permissions"]["allow"] = self._merge_permissions_allow(
            source.get("permissions", {}).get("allow", []),
            target.get("permissions", {}).get("allow", []),
        )

        # Deny list: preserve target (security boundary)
        result["permissions"]["deny"] = target.get("permissions", {}).get("deny", [])

        # Ask list: union
        source_ask = source.get("permissions", {}).get("ask", [])
        target_ask = target.get("permissions", {}).get("ask", [])
        result["permissions"]["ask"] = list(set(source_ask + target_ask))

        # Merge hooks
        result["hooks"] = self._merge_hooks(source.get("hooks", {}), target.get("hooks", {}))

        # Merge all other top-level keys from source (e.g., statusLine, etc.)
        # Source takes precedence for configuration keys
        for key in source:
            if key not in ("permissions", "hooks"):
                result[key] = source[key]

        # Preserve any target-only top-level keys that aren't in source
        for key in target:
            if key not in result and key not in ("permissions", "hooks"):
                result[key] = target[key]

        # Return with sorted keys for consistent output
        return dict(sorted(result.items()))

    def _strict_merge(self, source: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]:
        """Strict merge: source overwrites, except deny list."""
        result = json.loads(json.dumps(source))  # Deep copy

        # Preserve target's deny list (security boundary)
        if "permissions" in result and "permissions" in target:
            result["permissions"]["deny"] = target["permissions"].get("deny", [])

        # Return with sorted keys for consistent output
        return dict(sorted(result.items()))

    def _merge_permissions_allow(
        self, source_perms: list[str], target_perms: list[str]
    ) -> list[str]:
        """
        Merge allow permissions with intelligent deduplication.

        Rules:
        1. Remove exact duplicates
        2. Handle wildcard patterns (e.g., "Bash(git :*)" subsumes "Bash(git status)")
        3. Preserve order: source first, then unique target perms
        """
        # Parse permissions into structured format
        parsed_source = [self._parse_permission(p) for p in source_perms]
        parsed_target = [self._parse_permission(p) for p in target_perms]

        # Start with all source permissions
        result = parsed_source.copy()

        # Add target permissions that aren't subsumed by source
        for target_perm in parsed_target:
            if not self._is_subsumed(target_perm, parsed_source):
                result.append(target_perm)

        # Convert back to string format
        return [self._format_permission(p) for p in result]

    def _parse_permission(self, perm: str) -> dict[str, Any]:
        """
        Parse permission string into structured format.

        Examples:
            "Read" -> {"tool": "Read", "args": None}
            "Bash(git status)" -> {"tool": "Bash", "args": ["git", "status"]}
            "Bash(git :*)" -> {"tool": "Bash", "args": ["git", ":*"]}
        """
        if "(" not in perm:
            return {"tool": perm, "args": None}

        tool, args_str = perm.split("(", 1)
        args_str = args_str.rstrip(")")
        args = [a.strip() for a in args_str.split()]

        return {"tool": tool, "args": args}

    def _format_permission(self, perm: dict[str, Any]) -> str:
        """Convert structured permission back to string."""
        if perm["args"] is None:
            return str(perm["tool"])

        args_str = " ".join(perm["args"])
        return f"{perm['tool']}({args_str})"

    def _is_subsumed(self, perm: dict[str, Any], existing: list[dict[str, Any]]) -> bool:
        """
        Check if permission is subsumed by existing permissions.

        Examples:
            "Bash(git status)" is subsumed by "Bash(git :*)"
            "Bash(git log)" is subsumed by "Bash(git :*)"
            "Read" is NOT subsumed by "Bash(:*)"
        """
        for exist in existing:
            # Exact match
            if perm == exist:
                return True

            # Check if exist has wildcard that covers perm
            if exist["tool"] == perm["tool"]:
                if exist["args"] is None:
                    # Tool-level wildcard covers everything (unlikely but handle it)
                    return True

                if perm["args"] is None:
                    # Specific perm has no args, not subsumed by arg-specific exist
                    continue

                # Check if exist has wildcard in matching position
                if self._wildcard_matches(exist["args"], perm["args"]):
                    return True

        return False

    def _wildcard_matches(self, pattern: list[str], args: list[str]) -> bool:
        """
        Check if wildcard pattern matches args.

        Rules:
        - ":*" in pattern matches any number of args from that position
        - Must match prefix exactly before wildcard

        Examples:
            pattern=["git", ":*"], args=["git", "status"] -> True
            pattern=["git", ":*"], args=["git", "log", "--oneline"] -> True
            pattern=["git", ":*"], args=["npm", "test"] -> False
            pattern=["gh", "pr", ":*"], args=["gh", "pr", "view", "123"] -> True
        """
        for i, pattern_part in enumerate(pattern):
            if pattern_part == ":*":
                # Wildcard matches rest - check that prefix matched
                return True

            if i >= len(args):
                # Pattern longer than args and no wildcard
                return False

            if pattern_part != args[i]:
                # Mismatch before wildcard
                return False

        # All pattern parts matched - check if args exactly same length
        return len(pattern) == len(args)

    def _merge_hooks(
        self, source_hooks: dict[str, Any], target_hooks: dict[str, Any]
    ) -> dict[str, Any]:
        """
        Merge hooks by event type and matcher.

        Hooks are grouped by event type (PostToolUse, etc.) and then by matcher pattern.
        If the same matcher exists in both, keep both hooks (they'll run sequentially).

        Example:
            Source: PostToolUse with matcher "Bash(gh pr create :*)"
            Target: PostToolUse with matcher "Bash(npm test)"
            Result: Both hooks preserved under PostToolUse

        Returns:
            Merged hooks dictionary
        """
        result = {}

        # Get all unique event types
        all_events = set(source_hooks.keys()) | set(target_hooks.keys())

        for event in all_events:
            source_entries = source_hooks.get(event, [])
            target_entries = target_hooks.get(event, [])

            # Build map by matcher
            hook_map = {}

            # Add source hooks
            for entry in source_entries:
                matcher = entry.get("matcher", "")
                hook_map[matcher] = entry

            # Add target hooks (don't duplicate same matcher)
            for entry in target_entries:
                matcher = entry.get("matcher", "")
                if matcher not in hook_map:
                    hook_map[matcher] = entry
                # If matcher exists, source wins (don't duplicate)

            result[event] = list(hook_map.values())

        return result

    def compute_diff(
        self, source: dict[str, Any], target: dict[str, Any] | None, mode: str = "union"
    ) -> dict[str, Any]:
        """
        Compute what would change without actually merging.

        Returns:
            Dictionary with added/removed permissions and hooks
        """
        if target is None:
            return {
                "permissions_added": source.get("permissions", {}).get("allow", []),
                "permissions_removed": [],
                "hooks_added": list(source.get("hooks", {}).keys()),
                "hooks_removed": [],
            }

        merged = self.merge(source, target, mode)

        # Compare permissions
        target_perms = set(target.get("permissions", {}).get("allow", []))
        merged_perms = set(merged.get("permissions", {}).get("allow", []))

        permissions_added = list(merged_perms - target_perms)
        permissions_removed = list(target_perms - merged_perms) if mode == "strict" else []

        # Compare hooks
        target_hook_keys = set(target.get("hooks", {}).keys())
        merged_hook_keys = set(merged.get("hooks", {}).keys())

        hooks_added = list(merged_hook_keys - target_hook_keys)
        hooks_removed = list(target_hook_keys - merged_hook_keys) if mode == "strict" else []

        return {
            "permissions_added": permissions_added,
            "permissions_removed": permissions_removed,
            "hooks_added": hooks_added,
            "hooks_removed": hooks_removed,
        }


def load_settings(path: Path) -> dict[str, Any] | None:
    """Load settings.json from path."""
    if not path.exists():
        return None

    with open(path) as f:
        result: dict[str, Any] = json.load(f)
        return result


def save_settings(path: Path, settings: dict[str, Any]) -> None:
    """Save settings.json to path with proper formatting."""
    # Sort top-level keys for consistent output
    sorted_settings = dict(sorted(settings.items()))
    with open(path, "w") as f:
        json.dump(sorted_settings, f, indent=2)
        f.write("\n")  # Add trailing newline
