"""Template validation for TokenMoulds MCP server.

Provides ISO 29500 compliance validation and OOXML/ODF structure checking.
Wraps the bin/validate-template script and provides additional validation.
"""

from __future__ import annotations

import zipfile
from dataclasses import dataclass, field
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, List, Optional

from lxml import etree

from tokenmoulds.validation.ooxml import validate_ooxml_bytes
from tokenmoulds.validation.structural import validate_package_structure


@dataclass
class ValidationResult:
    """Result of template validation."""

    valid: bool
    format: str  # ooxml, odf, thmx
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    info: List[str] = field(default_factory=list)
    iso_compliant: bool = False
    structure: Optional[Dict[str, Any]] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "valid": self.valid,
            "format": self.format,
            "errors": self.errors,
            "warnings": self.warnings,
            "info": self.info,
            "iso_compliant": self.iso_compliant,
            "structure": self.structure,
        }


class TemplateValidator:
    """Validates Office templates for ISO 29500 compliance.

    Performs structural validation of OOXML and ODF packages,
    checking for required parts, valid relationships, and content types.
    """

    # Required content types for OOXML formats
    REQUIRED_CONTENT_TYPES = {
        "word": [
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
            "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
        ],
        "word_template": [
            "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml",
            "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
        ],
        "powerpoint": [
            "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
        ],
        "powerpoint_template": [
            "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml",
        ],
        "excel": [
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
        ],
        "excel_template": [
            "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml",
        ],
        "theme": [
            "application/vnd.openxmlformats-officedocument.theme+xml",
        ],
    }

    # Required parts for OOXML
    REQUIRED_PARTS = {
        "word": ["word/document.xml", "word/styles.xml", "[Content_Types].xml"],
        "word_template": [
            "word/document.xml",
            "word/styles.xml",
            "[Content_Types].xml",
        ],
        "powerpoint": ["ppt/presentation.xml", "[Content_Types].xml"],
        "powerpoint_template": ["ppt/presentation.xml", "[Content_Types].xml"],
        "excel": ["xl/workbook.xml", "[Content_Types].xml"],
        "excel_template": ["xl/workbook.xml", "[Content_Types].xml"],
        "theme": ["theme/theme1.xml", "[Content_Types].xml"],
    }

    def __init__(self, validate_script: Optional[Path] = None) -> None:
        """Initialize validator.

        Args:
            validate_script: Path to bin/validate-template script
        """
        if validate_script is None:
            # Try to find it relative to this file
            module_path = Path(__file__).parent.parent.parent.parent
            validate_script = module_path / "bin" / "validate-template"

        self.validate_script = validate_script

    def validate(
        self,
        template_data: bytes,
        format_hint: Optional[str] = None,
        strict_mode: bool = True,
        extract_structure: bool = False,
    ) -> ValidationResult:
        """Validate a template.

        Args:
            template_data: Template bytes
            format_hint: Format hint (word, powerpoint, excel, theme, odf)
            strict_mode: Whether to use strict ISO 29500 validation
            extract_structure: Whether to extract template structure

        Returns:
            ValidationResult with validation details
        """
        # Detect format
        detected_format = self._detect_format(template_data, format_hint)

        # Initialize result
        result = ValidationResult(
            valid=True,
            format=detected_format,
        )

        # Run appropriate validation
        if detected_format in (
            "word",
            "word_template",
            "powerpoint",
            "powerpoint_template",
            "excel",
            "excel_template",
            "theme",
        ):
            self._validate_ooxml(template_data, detected_format, result, strict_mode)
        elif detected_format in ("writer", "impress", "calc"):
            self._validate_odf(template_data, detected_format, result)
        else:
            result.errors.append(f"Unknown format: {detected_format}")
            result.valid = False

        # Extract structure if requested
        if extract_structure:
            result.structure = self._extract_structure(template_data, detected_format)

        return result

    def _detect_format(self, data: bytes, hint: Optional[str] = None) -> str:
        """Detect template format from bytes.

        Args:
            data: Template bytes
            hint: Format hint

        Returns:
            Format string
        """
        if hint:
            return hint

        # Check if it's a ZIP file
        if not data[:4] == b"PK\x03\x04":
            return "unknown"

        try:
            with zipfile.ZipFile(BytesIO(data), "r") as zf:
                names = zf.namelist()

                # Check for OOXML formats
                if "[Content_Types].xml" in names:
                    content_types = zf.read("[Content_Types].xml").decode("utf-8")

                    if "wordprocessingml.template" in content_types:
                        return "word_template"
                    if "wordprocessingml.document" in content_types:
                        return "word"
                    if "presentationml.template" in content_types:
                        return "powerpoint_template"
                    if "presentationml.presentation" in content_types:
                        return "powerpoint"
                    if "spreadsheetml.template" in content_types:
                        return "excel_template"
                    if "spreadsheetml.sheet" in content_types:
                        return "excel"
                    if "theme+xml" in content_types and "theme/theme1.xml" in names:
                        return "theme"

                # Check for ODF formats
                if "mimetype" in names:
                    mimetype = zf.read("mimetype").decode("utf-8").strip()
                    if "opendocument.text" in mimetype:
                        return "writer"
                    if "opendocument.presentation" in mimetype:
                        return "impress"
                    if "opendocument.spreadsheet" in mimetype:
                        return "calc"

        except zipfile.BadZipFile:
            pass

        return "unknown"

    def _validate_ooxml(
        self,
        data: bytes,
        format: str,
        result: ValidationResult,
        strict_mode: bool,
    ) -> None:
        """Validate OOXML package structure.

        Args:
            data: Template bytes
            format: Template format
            result: ValidationResult to populate
            strict_mode: Whether to use strict validation
        """
        try:
            with zipfile.ZipFile(BytesIO(data), "r") as zf:
                names = set(zf.namelist())
        except zipfile.BadZipFile:
            result.errors.append("Invalid ZIP package")
            result.valid = False
            return

        # Keep the format-specific required-part check local. It is a fast
        # packaging sanity rule, distinct from the deeper SDK-style pass.
        required = self.REQUIRED_PARTS.get(format, [])
        for part in required:
            if part not in names:
                result.errors.append(f"Missing required part: {part}")

        structural_report = validate_package_structure(data, format)
        self._append_validation_report(structural_report, result)

        if strict_mode and not result.errors:
            deep_outcome = validate_ooxml_bytes(
                data,
                format,
                include_binary=False,
            )
            self._append_validation_report(deep_outcome.report, result)
            if deep_outcome.executed and deep_outcome.report.is_valid:
                result.iso_compliant = True
                result.info.append("Template passed openxml-audit OOXML validation")

        result.valid = len(result.errors) == 0

    def _append_validation_report(self, report: Any, result: ValidationResult) -> None:
        """Copy unified validation issues into the MCP-facing result shape."""
        for issue in report.errors:
            result.errors.append(self._format_issue(issue))
        for issue in report.warnings:
            result.warnings.append(self._format_issue(issue))
        for issue in report.infos:
            result.info.append(self._format_issue(issue))

    def _format_issue(self, issue: Any) -> str:
        """Format a unified validation issue as a human-readable string."""
        location = getattr(issue, "location", None)
        message = getattr(issue, "message", str(issue))
        return f"{location}: {message}" if location else message

    def _validate_content_types(
        self,
        zf: zipfile.ZipFile,
        format: str,
        result: ValidationResult,
    ) -> None:
        """Validate [Content_Types].xml."""
        content_types_xml = zf.read("[Content_Types].xml").decode("utf-8")

        required_types = self.REQUIRED_CONTENT_TYPES.get(format, [])
        for ct in required_types:
            if ct not in content_types_xml:
                result.warnings.append(f"Missing content type: {ct}")

    def _validate_relationships(
        self,
        zf: zipfile.ZipFile,
        result: ValidationResult,
    ) -> None:
        """Validate relationship files."""
        try:
            rels = zf.read("_rels/.rels")
            root = etree.fromstring(rels)

            # Check for required relationships
            rel_types = [rel.get("Type", "") for rel in root]

            # Should have at least one relationship
            if not rel_types:
                result.warnings.append("Root relationships file is empty")

        except etree.XMLSyntaxError as e:
            result.errors.append(f"Invalid root relationships XML: {e}")

    def _validate_strict_ooxml(
        self,
        zf: zipfile.ZipFile,
        format: str,
        result: ValidationResult,
    ) -> None:
        """Validate strict OOXML requirements (ISO 29500 strict mode)."""
        # Check for deprecated features
        deprecated_namespaces = [
            "urn:schemas-microsoft-com:office:",
            "urn:schemas-microsoft-com:vml",
        ]

        names = zf.namelist()
        for name in names:
            if name.endswith(".xml"):
                try:
                    content = zf.read(name).decode("utf-8")
                    for ns in deprecated_namespaces:
                        if ns in content:
                            result.warnings.append(
                                f"Deprecated namespace in {name}: {ns}"
                            )
                except Exception:
                    pass

    def _validate_odf(
        self,
        data: bytes,
        format: str,
        result: ValidationResult,
    ) -> None:
        """Validate ODF package structure.

        Args:
            data: Template bytes
            format: Template format
            result: ValidationResult to populate
        """
        try:
            with zipfile.ZipFile(BytesIO(data), "r") as zf:
                names = zf.namelist()

                # Check required files
                required = [
                    "mimetype",
                    "content.xml",
                    "styles.xml",
                    "META-INF/manifest.xml",
                ]
                for part in required:
                    if part not in names:
                        result.errors.append(f"Missing required part: {part}")
                        result.valid = False

                # Check mimetype (must be uncompressed)
                if "mimetype" in names:
                    info = zf.getinfo("mimetype")
                    if info.compress_type != zipfile.ZIP_STORED:
                        result.warnings.append("mimetype should be uncompressed")

                # Check XML well-formedness
                for name in names:
                    if name.endswith(".xml"):
                        try:
                            content = zf.read(name)
                            etree.fromstring(content)
                        except etree.XMLSyntaxError as e:
                            result.errors.append(f"Invalid XML in {name}: {e}")
                            result.valid = False

                if result.valid and not result.errors:
                    result.iso_compliant = True  # ODF is ISO 26300
                    result.info.append("Template is ODF (ISO 26300) compliant")

        except zipfile.BadZipFile:
            result.errors.append("Invalid ZIP package")
            result.valid = False

    def _extract_structure(
        self,
        data: bytes,
        format: str,
    ) -> Dict[str, Any]:
        """Extract template structure for inspection.

        Args:
            data: Template bytes
            format: Template format

        Returns:
            Dictionary with structure info
        """
        structure = {
            "format": format,
            "parts": [],
            "content_types": [],
            "relationships": [],
        }

        try:
            with zipfile.ZipFile(BytesIO(data), "r") as zf:
                # List all parts
                for info in zf.infolist():
                    structure["parts"].append(
                        {
                            "name": info.filename,
                            "size": info.file_size,
                            "compressed_size": info.compress_size,
                        }
                    )

                # Extract content types
                if "[Content_Types].xml" in zf.namelist():
                    ct = zf.read("[Content_Types].xml")
                    root = etree.fromstring(ct)
                    for el in root:
                        tag = el.tag.split("}")[-1] if "}" in el.tag else el.tag
                        structure["content_types"].append(
                            {
                                "type": tag,
                                "extension": el.get("Extension"),
                                "part_name": el.get("PartName"),
                                "content_type": el.get("ContentType"),
                            }
                        )

                # Extract root relationships
                if "_rels/.rels" in zf.namelist():
                    rels = zf.read("_rels/.rels")
                    root = etree.fromstring(rels)
                    for rel in root:
                        structure["relationships"].append(
                            {
                                "id": rel.get("Id"),
                                "type": rel.get("Type"),
                                "target": rel.get("Target"),
                            }
                        )

        except Exception:
            pass

        return structure


def validate_template(
    template_data: bytes,
    format_hint: Optional[str] = None,
    strict_mode: bool = True,
    extract_structure: bool = False,
) -> ValidationResult:
    """Convenience function to validate a template.

    Args:
        template_data: Template bytes
        format_hint: Format hint
        strict_mode: Whether to use strict validation
        extract_structure: Whether to extract structure

    Returns:
        ValidationResult
    """
    validator = TemplateValidator()
    return validator.validate(
        template_data,
        format_hint=format_hint,
        strict_mode=strict_mode,
        extract_structure=extract_structure,
    )
