"""SuperTheme Quality Validator.

Validates SuperTheme packages against Microsoft-tier quality rules.
Use this to ensure generated themes meet the standards documented
in supertheme-format.md.
"""

from __future__ import annotations

import colorsys
import zipfile
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from pathlib import Path
from collections.abc import Mapping
from typing import Dict, List, Optional, Tuple

import structlog

from tokenmoulds.validation.model import Severity

logger = structlog.get_logger(__name__)


@dataclass
class ValidationIssue:
    """A single validation issue."""

    code: str
    message: str
    severity: Severity
    location: Optional[str] = None
    suggestion: Optional[str] = None


@dataclass
class ValidationResult:
    """Complete validation result for a SuperTheme."""

    is_valid: bool
    issues: List[ValidationIssue] = field(default_factory=list)
    summary: Dict[str, int] = field(default_factory=dict)

    def add_issue(self, issue: ValidationIssue) -> None:
        self.issues.append(issue)
        self.summary[issue.severity.value] = (
            self.summary.get(issue.severity.value, 0) + 1
        )
        if issue.severity == Severity.ERROR:
            self.is_valid = False

    @property
    def errors(self) -> List[ValidationIssue]:
        return [i for i in self.issues if i.severity == Severity.ERROR]

    @property
    def warnings(self) -> List[ValidationIssue]:
        return [i for i in self.issues if i.severity == Severity.WARNING]

    @property
    def infos(self) -> List[ValidationIssue]:
        return [i for i in self.issues if i.severity == Severity.INFO]


# XML Namespaces
_DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
_THEMEML_NS = "http://schemas.microsoft.com/office/thememl/2012/main"
_PRES_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"


def _hex_to_hsl(hex_color: str) -> Tuple[float, float, float]:
    """Convert hex color to HSL (hue 0-360, saturation 0-100, lightness 0-100)."""
    hex_color = hex_color.lstrip("#").upper()
    if len(hex_color) != 6:
        return (0, 0, 50)
    try:
        r = int(hex_color[0:2], 16) / 255
        g = int(hex_color[2:4], 16) / 255
        b = int(hex_color[4:6], 16) / 255
        hue, lightness, saturation = colorsys.rgb_to_hls(r, g, b)
        return (hue * 360, saturation * 100, lightness * 100)
    except ValueError:
        return (0, 0, 50)


def _extract_color_value(slot: ET.Element) -> Optional[str]:
    """Extract color value from a color slot element."""
    ns = {"a": _DML_NS}
    sys_clr = slot.find("a:sysClr", ns)
    if sys_clr is not None:
        return sys_clr.get("lastClr", "").upper()
    srgb_clr = slot.find("a:srgbClr", ns)
    if srgb_clr is not None:
        return srgb_clr.get("val", "").upper()
    return None


class SuperThemeValidator:
    """Validates SuperTheme packages against quality rules."""

    MAX_RECOMMENDED_LAYOUTS = 11

    def __init__(self) -> None:
        self.result = ValidationResult(is_valid=True)

    def validate(self, path: str | Path) -> ValidationResult:
        """Validate a SuperTheme package.

        Args:
            path: Path to .thmx file

        Returns:
            ValidationResult with all issues found
        """
        path = Path(path)
        self.result = ValidationResult(is_valid=True)

        if not path.exists():
            self.result.add_issue(
                ValidationIssue(
                    code="FILE_NOT_FOUND",
                    message=f"File not found: {path}",
                    severity=Severity.ERROR,
                )
            )
            return self.result

        try:
            with zipfile.ZipFile(path, "r") as zf:
                self._validate_structure(zf)
                self._validate_variant_manager(zf)
                self._validate_theme_family_consistency(zf)
                self._validate_variant_ordering(zf)
                self._validate_colors(zf)
                self._validate_layouts(zf)
                self._validate_thumbnails(zf)
        except zipfile.BadZipFile:
            self.result.add_issue(
                ValidationIssue(
                    code="INVALID_ARCHIVE",
                    message="File is not a valid ZIP archive",
                    severity=Severity.ERROR,
                )
            )

        return self.result

    def _validate_structure(self, zf: zipfile.ZipFile) -> None:
        """Validate basic SuperTheme structure."""
        required_files = [
            "[Content_Types].xml",
            "_rels/.rels",
            "themeVariants/themeVariantManager.xml",
        ]

        names = set(zf.namelist())
        for req in required_files:
            if req not in names:
                self.result.add_issue(
                    ValidationIssue(
                        code="MISSING_FILE",
                        message=f"Required file missing: {req}",
                        severity=Severity.ERROR,
                        location=req,
                    )
                )

    def _validate_variant_manager(self, zf: zipfile.ZipFile) -> None:
        """Validate themeVariantManager.xml."""
        try:
            content = zf.read("themeVariants/themeVariantManager.xml").decode(
                "utf-8-sig"
            )
            root = ET.fromstring(content)
            ns = {"t": _THEMEML_NS}

            variants = root.findall(".//t:themeVariant", ns)
            if not variants:
                self.result.add_issue(
                    ValidationIssue(
                        code="NO_VARIANTS",
                        message="No variants found in themeVariantManager.xml",
                        severity=Severity.ERROR,
                        location="themeVariants/themeVariantManager.xml",
                    )
                )
                return

            # Check variant count
            variant_count = len(variants)
            if variant_count > 16:
                self.result.add_issue(
                    ValidationIssue(
                        code="TOO_MANY_VARIANTS",
                        message=f"Too many variants ({variant_count}). Recommend 4-8 for clean UI.",
                        severity=Severity.WARNING,
                        location="themeVariants/themeVariantManager.xml",
                        suggestion="Consider limiting to 8 color variants maximum",
                    )
                )

            # Check for required attributes
            for i, variant in enumerate(variants):
                vid = variant.get("vid")
                name = variant.get("name")
                cx = variant.get("cx")
                cy = variant.get("cy")

                if not vid:
                    self.result.add_issue(
                        ValidationIssue(
                            code="MISSING_VID",
                            message=f"Variant {i + 1} missing 'vid' attribute",
                            severity=Severity.ERROR,
                            location="themeVariants/themeVariantManager.xml",
                        )
                    )
                if not name:
                    self.result.add_issue(
                        ValidationIssue(
                            code="MISSING_NAME",
                            message=f"Variant {i + 1} missing 'name' attribute",
                            severity=Severity.WARNING,
                            location="themeVariants/themeVariantManager.xml",
                        )
                    )
                if not cx or not cy:
                    self.result.add_issue(
                        ValidationIssue(
                            code="MISSING_DIMENSIONS",
                            message=f"Variant {i + 1} missing dimensions (cx/cy)",
                            severity=Severity.ERROR,
                            location="themeVariants/themeVariantManager.xml",
                        )
                    )

        except KeyError:
            pass  # Already handled in structure validation

    def _validate_theme_family_consistency(self, zf: zipfile.ZipFile) -> None:
        """Validate themeFamily name is identical across all variants."""
        ns = {"a": _DML_NS, "thm15": _THEMEML_NS}
        theme_family_names: Dict[str, str] = {}
        theme_family_ids: Dict[str, str] = {}

        for name in zf.namelist():
            if name.endswith("theme1.xml"):
                try:
                    content = zf.read(name).decode("utf-8-sig")
                    root = ET.fromstring(content)

                    family = root.find(".//thm15:themeFamily", ns)
                    if family is not None:
                        family_name = family.get("name", "")
                        family_id = family.get("id", "")
                        theme_family_names[name] = family_name
                        theme_family_ids[name] = family_id
                except Exception:
                    pass

        # Check name consistency
        unique_names = set(theme_family_names.values())
        if len(unique_names) > 1:
            self.result.add_issue(
                ValidationIssue(
                    code="INCONSISTENT_FAMILY_NAME",
                    message=f"themeFamily name varies across variants: {unique_names}",
                    severity=Severity.WARNING,
                    suggestion="Use the same themeFamily name for all variants",
                )
            )

        # Check ID consistency (critical)
        unique_ids = set(theme_family_ids.values())
        if len(unique_ids) > 1:
            self.result.add_issue(
                ValidationIssue(
                    code="INCONSISTENT_FAMILY_ID",
                    message="themeFamily id differs across variants - PowerPoint may not recognize them as related",
                    severity=Severity.ERROR,
                    suggestion="Use identical themeFamily id for all variants",
                )
            )

    def _validate_variant_ordering(self, zf: zipfile.ZipFile) -> None:
        """Validate variants are ordered size-first, then by design."""
        try:
            content = zf.read("themeVariants/themeVariantManager.xml").decode(
                "utf-8-sig"
            )
            root = ET.fromstring(content)
            ns = {"t": _THEMEML_NS}

            variants = root.findall(".//t:themeVariant", ns)

            # Extract dimensions
            dims = []
            for v in variants:
                cx = int(v.get("cx", "0"))
                cy = int(v.get("cy", "0"))
                dims.append((cx, cy))

            # Check if size-first ordering
            if len(dims) > 1:
                current_size = dims[0]
                sizes_seen = [current_size]

                for i, dim in enumerate(dims[1:], start=1):
                    if dim != current_size:
                        if dim in sizes_seen:
                            # Size repeated after changing - not size-first
                            self.result.add_issue(
                                ValidationIssue(
                                    code="NOT_SIZE_FIRST_ORDER",
                                    message="Variants are not ordered size-first (group all designs per size)",
                                    severity=Severity.INFO,
                                    suggestion="Order: all designs at 16:9, then all at 4:3, etc.",
                                )
                            )
                            break
                        sizes_seen.append(dim)
                        current_size = dim

        except Exception:
            pass

    def _validate_colors(self, zf: zipfile.ZipFile) -> None:
        """Validate color scheme rules."""
        ns = {"a": _DML_NS}

        for name in zf.namelist():
            if not name.endswith("theme1.xml"):
                continue

            try:
                content = zf.read(name).decode("utf-8-sig")
                root = ET.fromstring(content)
                clr_scheme = root.find(".//a:clrScheme", ns)

                if clr_scheme is None:
                    continue

                colors = {}
                for slot_name in [
                    "dk1",
                    "lt1",
                    "dk2",
                    "lt2",
                    "accent1",
                    "accent2",
                    "accent3",
                    "accent4",
                    "accent5",
                    "accent6",
                    "hlink",
                    "folHlink",
                ]:
                    slot = clr_scheme.find(f"a:{slot_name}", ns)
                    if slot is not None:
                        colors[slot_name] = _extract_color_value(slot)

                # Validate dk1/lt1 softened extremes
                self._check_softened_extremes(colors, name)

                # Validate hlink == folHlink
                self._check_hyperlink_consistency(colors, name)

                # Validate accent3-6 are muted
                self._check_utility_accents(colors, name)

            except Exception as e:
                logger.debug("Error parsing theme", file=name, error=str(e))

    def _check_softened_extremes(
        self, colors: Mapping[str, Optional[str]], location: str
    ) -> None:
        """Check dk1/lt1 are not pure black/white."""
        dk1 = colors.get("dk1")
        lt1 = colors.get("lt1")

        if dk1 == "000000":
            self.result.add_issue(
                ValidationIssue(
                    code="PURE_BLACK_DK1",
                    message="dk1 is pure black (#000000)",
                    severity=Severity.INFO,
                    location=location,
                    suggestion="Use near-black (L 12-18%) for better dark mode and accessibility",
                )
            )

        if lt1 == "FFFFFF":
            self.result.add_issue(
                ValidationIssue(
                    code="PURE_WHITE_LT1",
                    message="lt1 is pure white (#FFFFFF)",
                    severity=Severity.INFO,
                    location=location,
                    suggestion="Use off-white (L 97-99%) for less glare",
                )
            )

    def _check_hyperlink_consistency(
        self, colors: Mapping[str, Optional[str]], location: str
    ) -> None:
        """Check hlink and folHlink match."""
        hlink = colors.get("hlink")
        folHlink = colors.get("folHlink")

        if hlink and folHlink and hlink != folHlink:
            self.result.add_issue(
                ValidationIssue(
                    code="MISMATCHED_HYPERLINKS",
                    message=f"hlink ({hlink}) differs from folHlink ({folHlink})",
                    severity=Severity.INFO,
                    location=location,
                    suggestion="Set folHlink == hlink to avoid 'visited' purple",
                )
            )

    def _check_utility_accents(
        self, colors: Mapping[str, Optional[str]], location: str
    ) -> None:
        """Check accent3-6 are muted utility colors."""
        accent1 = colors.get("accent1")
        if not accent1:
            return

        _, accent1_sat, _ = _hex_to_hsl(accent1)

        for slot in ["accent3", "accent4", "accent5", "accent6"]:
            color = colors.get(slot)
            if not color:
                continue

            _, sat, lightness = _hex_to_hsl(color)

            # Accent 3-6 should be less saturated than accent1
            if sat > accent1_sat * 0.8:
                self.result.add_issue(
                    ValidationIssue(
                        code="SATURATED_UTILITY_ACCENT",
                        message=f"{slot} ({color}) is too saturated for a utility slot",
                        severity=Severity.INFO,
                        location=location,
                        suggestion=f"{slot} should be a muted tint/shade or neutral",
                    )
                )

    def _validate_layouts(self, zf: zipfile.ZipFile) -> None:
        """Validate layout count and names."""
        layout_files = [
            n for n in zf.namelist() if "slideLayout" in n and n.endswith(".xml")
        ]

        # Count layouts per variant
        layout_counts: Dict[str, int] = {}
        for layout in layout_files:
            # Extract base path (theme/ or themeVariants/variant1/theme/)
            parts = layout.split("/slideLayouts/")
            if len(parts) == 2:
                base = parts[0]
                layout_counts[base] = layout_counts.get(base, 0) + 1

        for base, count in layout_counts.items():
            if count > self.MAX_RECOMMENDED_LAYOUTS:
                self.result.add_issue(
                    ValidationIssue(
                        code="TOO_MANY_LAYOUTS",
                        message=f"Too many layouts ({count}) in {base}",
                        severity=Severity.INFO,
                        suggestion=f"Microsoft recommends 9-11 layouts. Consider reducing from {count}.",
                    )
                )

    def _validate_thumbnails(self, zf: zipfile.ZipFile) -> None:
        """Validate thumbnail presence."""
        required_thumbnails = [
            "themeThumbnail.jpeg",
            "auxiliaryThemeThumbnail.jpeg",
            "auxiliaryThemeThumbnailMedium.jpeg",
        ]

        names = set(zf.namelist())

        # Check base theme thumbnails
        for thumb in required_thumbnails:
            expected = f"theme/theme/{thumb}"
            if expected not in names:
                self.result.add_issue(
                    ValidationIssue(
                        code="MISSING_THUMBNAIL",
                        message=f"Missing thumbnail: {expected}",
                        severity=Severity.WARNING,
                        location=expected,
                    )
                )

        # Check docProps thumbnail
        if "docProps/thumbnail.jpeg" not in names:
            self.result.add_issue(
                ValidationIssue(
                    code="MISSING_PACKAGE_THUMBNAIL",
                    message="Missing package thumbnail: docProps/thumbnail.jpeg",
                    severity=Severity.WARNING,
                )
            )


def validate_supertheme(path: str | Path) -> ValidationResult:
    """Validate a SuperTheme package.

    Args:
        path: Path to .thmx file

    Returns:
        ValidationResult with all issues found

    Example:
        result = validate_supertheme("MyTheme.thmx")
        if not result.is_valid:
            for error in result.errors:
                print(f"ERROR: {error.message}")
        for warning in result.warnings:
            print(f"WARNING: {warning.message}")
    """
    return SuperThemeValidator().validate(path)


def validate_design_variant(
    dk1: str,
    lt1: str,
    dk2: str,
    lt2: str,
    accent1: str,
    accent2: str,
    accent3: str,
    accent4: str,
    accent5: str,
    accent6: str,
    hlink: str,
    folHlink: str,
) -> ValidationResult:
    """Validate a color scheme without a full SuperTheme package.

    Args:
        dk1-folHlink: Color values (hex with or without #)

    Returns:
        ValidationResult with color-related issues
    """
    result = ValidationResult(is_valid=True)
    validator = SuperThemeValidator()
    validator.result = result

    colors = {
        "dk1": dk1.lstrip("#").upper(),
        "lt1": lt1.lstrip("#").upper(),
        "dk2": dk2.lstrip("#").upper(),
        "lt2": lt2.lstrip("#").upper(),
        "accent1": accent1.lstrip("#").upper(),
        "accent2": accent2.lstrip("#").upper(),
        "accent3": accent3.lstrip("#").upper(),
        "accent4": accent4.lstrip("#").upper(),
        "accent5": accent5.lstrip("#").upper(),
        "accent6": accent6.lstrip("#").upper(),
        "hlink": hlink.lstrip("#").upper(),
        "folHlink": folHlink.lstrip("#").upper(),
    }

    validator._check_softened_extremes(colors, "color_scheme")
    validator._check_hyperlink_consistency(colors, "color_scheme")
    validator._check_utility_accents(colors, "color_scheme")

    return validator.result


__all__ = [
    "ValidationIssue",
    "ValidationResult",
    "SuperThemeValidator",
    "validate_supertheme",
    "validate_design_variant",
]
