"""Template validation for TokenMoulds MCP server.

Provides ISO 29500 compliance validation and OOXML/ODF structure checking.
"""

from __future__ import annotations

import zipfile
from dataclasses import dataclass, field
from io import BytesIO
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) -> None:
        """Initialize validator."""

    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,
    )
