"""Service for managing color assignment (UI-agnostic)."""

from __future__ import annotations

import hashlib
import json
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

from portal.core.domain.models import (
    ColorPalette,
    ColorResult,
    ColorUsageStats,
    DataResult,
    WorktreeColor,
)

from ._color_palettes import DEFAULT_COLORS, HIGH_CONTRAST_COLORS

if TYPE_CHECKING:
    from typing import Any


class ColorService:
    """Service for managing color assignment (UI-agnostic)."""

    def __init__(self, palette: ColorPalette = ColorPalette.DEFAULT):
        self.palette = palette
        self.colors = self._get_palette_colors(palette)
        self.usage_stats_file = Path.home() / ".portal" / "color_usage.json"
        self._usage_stats: dict[str, ColorUsageStats] = self._load_usage_stats()

    async def assign_color(self, worktree_id: str) -> DataResult[WorktreeColor]:
        """Assign a deterministic color to a worktree.

        This method implements the ColorServiceProtocol interface.
        """
        if not worktree_id or not worktree_id.strip():
            return ColorResult.create_failure("Invalid worktree ID")

        try:
            color = self._assign_color_deterministic(worktree_id)
            self._update_usage_stats(color.name, worktree_id)
            return ColorResult.create_success(color)
        except Exception as e:
            return ColorResult.create_failure(f"Failed to assign color: {e}")

    async def get_color(self, worktree_id: str) -> DataResult[WorktreeColor]:
        """Get the color assigned to a worktree.

        This method implements the ColorServiceProtocol interface.
        """
        if not worktree_id or not worktree_id.strip():
            return ColorResult.create_failure("Invalid worktree ID")

        try:
            color = self._assign_color_deterministic(worktree_id)
            return ColorResult.create_success(color)
        except Exception as e:
            return ColorResult.create_failure(f"Failed to get color: {e}")

    def _strip_branch_prefix(self, worktree_name: str) -> str:
        """Strip common git branch prefixes for better color distribution.

        Removes prefixes like feat/, fix/, feature/, etc. so that the color
        is based on the unique part of the branch name.
        """
        prefixes = (
            "feat/",
            "feature/",
            "fix/",
            "bugfix/",
            "hotfix/",
            "chore/",
            "docs/",
            "refactor/",
            "test/",
            "tests/",
            "ci/",
            "build/",
            "perf/",
            "style/",
            "release/",
            "wip/",
        )
        for prefix in prefixes:
            if worktree_name.lower().startswith(prefix):
                return worktree_name[len(prefix) :]
        return worktree_name

    def _assign_color_deterministic(
        self, worktree_name: str, project_name: str | None = None
    ) -> WorktreeColor:
        """Assign a deterministic color to a worktree."""
        if project_name and self._should_use_lru():
            color = self._get_least_used_color(project_name)
            if color:
                self._update_usage_stats(color.name, project_name)
                return color

        # Use a better hash distribution for visually distinct colors
        well_distributed_colors = self._get_well_distributed_colors()

        # Strip common branch prefixes for better distribution
        name_for_hash = self._strip_branch_prefix(worktree_name)

        # Use multiple hash strategies to get better distribution
        # Try different hash algorithms to find one that gives good distribution
        hash1 = int(hashlib.md5(name_for_hash.encode()).hexdigest()[:8], 16)
        hash2 = int(hashlib.sha256(name_for_hash.encode()).hexdigest()[:8], 16)
        hash3 = sum(ord(c) for c in name_for_hash)  # Simple character sum

        # Combine hashes for better distribution
        # This helps avoid clustering that can happen with a single hash
        combined_hash = hash1 * 7 + hash2 * 13 + hash3 * 31

        color_index = combined_hash % len(well_distributed_colors)
        color = well_distributed_colors[color_index]

        if project_name:
            self._update_usage_stats(color.name, project_name)

        return color

    def _get_well_distributed_colors(self) -> list[WorktreeColor]:
        """Get colors ordered for maximum visual distinction."""
        # Order colors to maximize contrast between adjacent indices
        # Arranged to ensure neighboring colors are visually distinct
        color_order = [
            "Blue",  # 0: Primary blue
            "Red",  # 1: Primary red
            "Green",  # 2: Primary green
            "Yellow",  # 3: Primary yellow
            "Purple",  # 4: Secondary purple
            "Orange",  # 5: Secondary orange
            "Cyan",  # 6: Secondary cyan
            "Pink",  # 7: Tertiary pink
            "Teal",  # 8: Tertiary teal
            "Lime",  # 9: Tertiary lime
            "Magenta",  # 10: Rich magenta
            "Indigo",  # 11: Deep blue
            "Gold",  # 12: Bright gold
            "Violet",  # 13: Deep violet
            "Turquoise",  # 14: Blue-green
            "Coral",  # 15: Warm coral
            "Light Green",  # 16: Bright green
            "Deep Purple",  # 17: Dark purple
            "Sky Blue",  # 18: Light cyan
            "Amber",  # 19: Orange-yellow
            "Brown",  # 20: Earth tone
            "Light Blue",  # 21: Pale blue
            "Deep Orange",  # 22: Dark orange
            "Blue Grey",  # 23: Neutral grey
        ]

        # Create a mapping of colors by name
        color_map = {color.name: color for color in self.colors}

        # Return colors in the well-distributed order, falling back to original order
        # if some colors don't exist in the current palette
        distributed = []
        for name in color_order:
            if name in color_map:
                distributed.append(color_map[name])

        # Add any remaining colors that weren't in our order list
        for color in self.colors:
            if color not in distributed:
                distributed.append(color)

        return distributed

    def get_color_by_name(self, color_name: str) -> WorktreeColor | None:
        """Get a specific color by name."""
        for color in self.colors:
            if color.name.lower() == color_name.lower():
                return color
        return None

    def get_available_colors(
        self, used_colors: set[str], project_name: str | None = None
    ) -> list[WorktreeColor]:
        """Get list of colors not currently in use."""
        available = [color for color in self.colors if color.name not in used_colors]

        if project_name and self._usage_stats:
            available.sort(key=lambda c: self._get_color_usage_count(c.name, project_name))

        return available

    def get_contrast_safe_colors(self, min_contrast: float = 4.5) -> list[WorktreeColor]:
        """Get colors that meet minimum contrast requirements."""
        return [c for c in self.colors if c.contrast_ratio >= min_contrast]

    def calculate_color_properties(self, hex_color: str) -> dict[str, Any]:
        """Calculate all color properties from hex value."""
        hex_color = hex_color.lstrip("#")
        rgb: tuple[int, int, int] = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))  # type: ignore[assignment]
        hsl = self._rgb_to_hsl(*rgb)
        contrast = self._calculate_contrast_ratio(rgb)
        is_dark = self._calculate_luminance(rgb) < 0.5
        ansi_256 = self._rgb_to_ansi256(*rgb)
        ansi_code = self._get_basic_ansi(*rgb)

        return {
            "rgb": rgb,
            "hsl": hsl,
            "contrast_ratio": contrast,
            "is_dark": is_dark,
            "ansi_256": ansi_256,
            "ansi_code": ansi_code,
            "ansi_rgb": f"\033[38;2;{rgb[0]};{rgb[1]};{rgb[2]}m",
            "iterm_rgb": f"{rgb[0] / 255:.2f} {rgb[1] / 255:.2f} {rgb[2] / 255:.2f}",
        }

    def _get_palette_colors(self, palette: ColorPalette) -> list[WorktreeColor]:
        """Get colors for specified palette."""
        palette_map = {
            ColorPalette.DEFAULT: DEFAULT_COLORS,
            ColorPalette.HIGH_CONTRAST: HIGH_CONTRAST_COLORS,
        }
        return palette_map.get(palette, DEFAULT_COLORS)

    def _should_use_lru(self) -> bool:
        """Check if we should use LRU color assignment."""
        return bool(self._usage_stats)

    def _get_least_used_color(self, project_name: str) -> WorktreeColor | None:
        """Get the least recently used color for a project."""
        if not self.colors:
            return None

        color_usage = [
            (color, self._get_color_usage_count(color.name, project_name)) for color in self.colors
        ]
        color_usage.sort(key=lambda x: x[1])
        return color_usage[0][0] if color_usage else None

    def _get_color_usage_count(self, color_name: str, project_name: str) -> int:
        """Get usage count for a color in a project."""
        if color_name not in self._usage_stats:
            return 0

        stats = self._usage_stats[color_name]
        return stats.projects.count(project_name)

    def _update_usage_stats(self, color_name: str, project_name: str) -> None:
        """Update color usage statistics."""
        if color_name not in self._usage_stats:
            self._usage_stats[color_name] = ColorUsageStats(color_name=color_name)

        stats = self._usage_stats[color_name]
        stats.usage_count += 1
        stats.last_used = datetime.now()

        if project_name not in stats.projects:
            stats.projects.append(project_name)

        self._save_usage_stats()

    def _load_usage_stats(self) -> dict[str, ColorUsageStats]:
        """Load usage statistics from file."""
        if not self.usage_stats_file.exists():
            return {}

        try:
            with open(self.usage_stats_file, encoding="utf-8") as f:
                data = json.load(f)
                return {k: ColorUsageStats.model_validate(v) for k, v in data.items()}
        except (json.JSONDecodeError, OSError, ValueError):
            return {}

    def _save_usage_stats(self) -> None:
        """Save usage statistics to file."""
        try:
            self.usage_stats_file.parent.mkdir(parents=True, exist_ok=True)
            data = {k: v.model_dump(mode="json") for k, v in self._usage_stats.items()}

            with open(self.usage_stats_file, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
        except OSError:
            pass  # Silently fail on stats saving to not break main functionality

    def _rgb_to_hsl(self, r: int, g: int, b: int) -> tuple[int, int, int]:
        """Convert RGB to HSL."""
        r_norm, g_norm, b_norm = r / 255.0, g / 255.0, b / 255.0
        max_c = max(r_norm, g_norm, b_norm)
        min_c = min(r_norm, g_norm, b_norm)
        lightness = (max_c + min_c) / 2.0

        if max_c == min_c:
            h = 0.0  # achromatic
            s = 0.0
        else:
            d = max_c - min_c
            s = d / (2.0 - max_c - min_c) if lightness > 0.5 else d / (max_c + min_c)

            if max_c == r_norm:
                h = (g_norm - b_norm) / d + (6 if g_norm < b_norm else 0)
            elif max_c == g_norm:
                h = (b_norm - r_norm) / d + 2
            else:
                h = (r_norm - g_norm) / d + 4

            h /= 6

        return (int(h * 360), int(s * 100), int(lightness * 100))

    def _calculate_luminance(self, rgb: tuple[int, int, int]) -> float:
        """Calculate relative luminance for contrast ratio."""

        def channel_luminance(channel: int) -> float:
            c = channel / 255.0
            if c <= 0.03928:
                return c / 12.92
            return float(((c + 0.055) / 1.055) ** 2.4)

        r, g, b = rgb
        return (
            0.2126 * channel_luminance(r)
            + 0.7152 * channel_luminance(g)
            + 0.0722 * channel_luminance(b)
        )

    def _calculate_contrast_ratio(self, rgb: tuple[int, int, int]) -> float:
        """Calculate WCAG contrast ratio against white background."""
        l1 = 1.0  # White background luminance
        l2 = self._calculate_luminance(rgb)
        return (l1 + 0.05) / (l2 + 0.05)

    def _rgb_to_ansi256(self, r: int, g: int, b: int) -> int:
        """Convert RGB to nearest 256-color palette index."""
        if r == g == b:
            # Grayscale
            if r < 8:
                return 16
            if r > 248:
                return 231
            return round(((r - 8) / 247) * 24) + 232

        # Color cube (6x6x6)
        def to_6level(v: int) -> int:
            return round(v / 255 * 5)

        return int(16 + (36 * to_6level(r)) + (6 * to_6level(g)) + to_6level(b))

    def _get_basic_ansi(self, r: int, g: int, b: int) -> str:
        """Get basic ANSI color code."""
        colors = {
            (0, 0, 0): "30",  # Black
            (128, 0, 0): "31",  # Red
            (0, 128, 0): "32",  # Green
            (128, 128, 0): "33",  # Yellow
            (0, 0, 128): "34",  # Blue
            (128, 0, 128): "35",  # Magenta
            (0, 128, 128): "36",  # Cyan
            (192, 192, 192): "37",  # White
        }

        min_distance = float("inf")
        closest_code = "37"

        for (cr, cg, cb), code in colors.items():
            distance = ((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2) ** 0.5
            if distance < min_distance:
                min_distance = distance
                closest_code = code

        return closest_code
