"""
Pydantic type definitions for YAML schema validation.

This module provides type-safe validation for all schema files,
ensuring cross-file consistency for domains, routes, endpoints, and entry_points.

NOTE: All enum-like validation is performed at runtime against YAML definitions.
- Layouts are validated against layouts.yaml via LayoutSchema
- Auth levels are validated against field_options.yaml via FieldOptions
- User types are validated against rbac.yaml

NO HARDCODED ENUMS - everything validates against YAML at runtime.
"""

from __future__ import annotations

import re
from functools import lru_cache
from typing import Any, Literal
from urllib.parse import urlparse

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

# =============================================================================
# BASE TYPES - Reusable validated types
# =============================================================================


class DomainPattern(BaseModel):
    """Validated domain pattern (e.g., 'cineos.io', 'app.cineos.io')."""

    model_config = ConfigDict(frozen=True)

    value: str

    @field_validator("value")
    @classmethod
    def validate_domain(cls, v: str) -> str:
        # Allow wildcards and placeholders
        pattern = r"^[\w\-\.\*\{\}]+\.[a-z]{2,}$"
        # Also allow subdomains with wildcards like *.cineos.io
        if not re.match(pattern, v.replace("{", "").replace("}", ""), re.IGNORECASE):
            # Check if it's a valid domain with placeholders
            clean = re.sub(r"\{[^}]+\}", "placeholder", v)
            if not re.match(r"^[\w\-\.]+\.[a-z]{2,}$", clean, re.IGNORECASE):
                raise ValueError(f"Invalid domain pattern: {v}")
        return v

    def get_base_domain(self) -> str:
        """Extract base domain without subdomains."""
        parts = self.value.split(".")
        if len(parts) >= 2:
            return ".".join(parts[-2:])
        return self.value


class URLPattern(BaseModel):
    """Validated URL pattern with optional placeholders."""

    model_config = ConfigDict(frozen=True)

    value: str
    placeholders: list[str] = Field(default_factory=list)

    @field_validator("value")
    @classmethod
    def validate_url(cls, v: str) -> str:
        # Must start with / or https://
        if not v.startswith("/") and not v.startswith("http"):
            raise ValueError(f"URL must start with / or http(s)://: {v}")
        return v

    @model_validator(mode="after")
    def extract_placeholders(self) -> "URLPattern":
        """Extract placeholder names from URL pattern."""
        # Find {placeholder} patterns
        placeholders = re.findall(r"\{([^}]+)\}", self.value)
        # Find <placeholder> patterns (Django style)
        placeholders.extend(re.findall(r"<([^>:]+)(?::[^>]+)?(?:>)", self.value))
        object.__setattr__(self, "placeholders", placeholders)
        return self


class EndpointURL(BaseModel):
    """Fully qualified endpoint URL (e.g., https://api.lightwave-media.ltd/webhooks/email/sent/)."""

    model_config = ConfigDict(frozen=True)

    url: str
    domain: str = ""
    path: str = ""
    placeholders: list[str] = Field(default_factory=list)

    @field_validator("url")
    @classmethod
    def validate_endpoint(cls, v: str) -> str:
        if not v.startswith("http://") and not v.startswith("https://"):
            raise ValueError(f"Endpoint must be a full URL: {v}")
        return v

    @model_validator(mode="after")
    def parse_url(self) -> "EndpointURL":
        """Parse URL into components."""
        parsed = urlparse(self.url)
        object.__setattr__(self, "domain", parsed.netloc)
        object.__setattr__(self, "path", parsed.path)
        # Extract placeholders
        placeholders = re.findall(r"\{([^}]+)\}", self.url)
        object.__setattr__(self, "placeholders", placeholders)
        return self


class EntryPoint(BaseModel):
    """
    Entry point pattern.

    Supports two formats:
    - String: 'admin.lightwave-media.ltd/lwm-admin/*'
    - Dict: {url: 'https://cineos.io/signup/', tenant: 'cineos'}
    """

    model_config = ConfigDict(frozen=True)

    value: str  # Original value (URL string)
    domain: str = ""
    path: str = ""
    tenant: str | None = None
    is_wildcard: bool = False

    @field_validator("value", mode="before")
    @classmethod
    def extract_value(cls, v: Any) -> str:
        """Extract URL string from dict format if needed."""
        if isinstance(v, dict):
            return v.get("url", str(v))
        return str(v)

    @model_validator(mode="before")
    @classmethod
    def handle_dict_format(cls, data: Any) -> Any:
        """Handle dict format entry points."""
        if isinstance(data, dict) and "url" in data:
            # Entry point is in dict format: {url: ..., tenant: ...}
            return {
                "value": data["url"],
                "tenant": data.get("tenant"),
            }
        elif isinstance(data, str):
            return {"value": data}
        return data

    @model_validator(mode="after")
    def parse_entry_point(self) -> "EntryPoint":
        """Parse entry point into domain and path."""
        # Handle full URLs
        if self.value.startswith("http"):
            parsed = urlparse(self.value)
            object.__setattr__(self, "domain", parsed.netloc)
            object.__setattr__(self, "path", parsed.path)
        else:
            # Handle domain/path format
            parts = self.value.split("/", 1)
            object.__setattr__(self, "domain", parts[0])
            object.__setattr__(self, "path", "/" + parts[1] if len(parts) > 1 else "/")

        object.__setattr__(self, "is_wildcard", "*" in self.value)
        return self


# =============================================================================
# KNOWN VALID VALUES - Derived from SST YAML at runtime
# =============================================================================


@lru_cache(maxsize=1)
def _get_valid_layouts() -> frozenset[str]:
    """Get valid layout names from SST YAML (cached)."""
    try:
        from lightwave.schema.pydantic.sst import LayoutSchema

        return frozenset(LayoutSchema.get_valid_layout_names())
    except (ImportError, FileNotFoundError):
        # Fallback if SST not available
        return frozenset(["web", "auth", "app", "app_admin", "error"])


@lru_cache(maxsize=1)
def _get_valid_auth_levels() -> frozenset[str]:
    """Get valid auth levels from field_options.yaml (cached)."""
    try:
        from lightwave.schema.pydantic.core.validators import FieldOptions

        values = FieldOptions.get_values("auth_levels")
        # Include None/null as valid
        return frozenset([v for v in values if v is not None])
    except (ImportError, FileNotFoundError, KeyError):
        # Fallback if SST not available
        return frozenset(["tenant_subscriber", "platform_admin"])


class PlatformRegistry(BaseModel):
    """
    Registry of all known valid domains, tenants, and backend services.

    NOTE: This class validates against SST YAML definitions at runtime.
    Layout and user type validation delegates to SST schemas and FieldOptions.
    The frozenset fields can be populated from YAML but validation methods
    will also check against the canonical SST definitions.
    """

    model_config = ConfigDict(frozen=True)

    # Tenant slugs
    tenants: frozenset[str] = frozenset()

    # All valid domains (marketing, app, store, etc.)
    tenant_domains: frozenset[str] = frozenset()

    # Backend service domains (*.lightwave-media.ltd)
    backend_domains: frozenset[str] = frozenset()

    # Webhook paths
    webhook_paths: frozenset[str] = frozenset()

    # All valid routes patterns
    route_patterns: frozenset[str] = frozenset()

    # Feature flags
    feature_flags: frozenset[str] = frozenset()

    # User types (from rbac.yaml) - loaded dynamically
    user_types: frozenset[str] = frozenset()

    # Layouts - loaded dynamically from layouts.yaml
    layouts: frozenset[str] = frozenset()

    def is_valid_domain(self, domain: str) -> bool:
        """Check if a domain is valid (tenant or backend)."""
        # Remove wildcards for comparison
        clean_domain = domain.replace("*.", "").replace("*", "")
        return (
            clean_domain in self.tenant_domains
            or clean_domain in self.backend_domains
            or any(clean_domain.endswith(f".{bd}") for bd in self.backend_domains)
        )

    def is_valid_tenant(self, tenant: str) -> bool:
        """Check if a tenant slug is valid."""
        return tenant in self.tenants

    def is_valid_webhook_path(self, path: str) -> bool:
        """Check if a webhook path is valid."""
        # Extract first path segment
        clean_path = path.strip("/").split("/")[0]
        return clean_path in self.webhook_paths

    def is_valid_user_type(self, user_type: str | None) -> bool:
        """
        Check if a user type is valid.

        Validates against auth_levels from field_options.yaml.
        """
        if user_type is None or user_type == "null":
            return True  # null means no auth required
        if user_type in ("dynamic", "varies"):
            return True  # Dynamic auth level

        # Check against both provided user_types and SST auth_levels
        if self.user_types and user_type in self.user_types:
            return True

        # Validate against SST field_options.yaml
        valid_auth_levels = _get_valid_auth_levels()
        return user_type in valid_auth_levels

    def is_valid_layout(self, layout: str | None) -> bool:
        """
        Check if a layout is valid.

        Validates against layouts.yaml via LayoutSchema.
        """
        if layout in ("dynamic", "null", None):
            return True

        # Check against both provided layouts and SST layouts
        if self.layouts and layout in self.layouts:
            return True

        # Validate against SST layouts.yaml
        valid_layouts = _get_valid_layouts()
        return layout in valid_layouts

    @classmethod
    def from_sst(cls) -> "PlatformRegistry":
        """
        Create a PlatformRegistry populated from SST YAML files.

        Loads layouts and auth levels from their canonical SST sources.

        Returns:
            PlatformRegistry with values from SST YAML
        """
        return cls(
            layouts=_get_valid_layouts(),
            # user_types here means auth_levels from field_options
            user_types=_get_valid_auth_levels(),
        )


# =============================================================================
# CROSS-REFERENCE VALIDATION RESULTS
# =============================================================================


class ValidationIssue(BaseModel):
    """A single validation issue."""

    model_config = ConfigDict(frozen=True)

    file: str
    location: str  # e.g., "webhook_events.email_sent.endpoint"
    issue_type: Literal[
        "invalid_domain",
        "invalid_endpoint",
        "invalid_tenant",
        "invalid_user_type",
        "invalid_layout",
        "invalid_webhook_path",
        "orphan_reference",
        "type_mismatch",
    ]
    message: str
    value: str  # Will be converted to string if needed
    suggestion: str | None = None

    @field_validator("value", mode="before")
    @classmethod
    def convert_value_to_str(cls, v: Any) -> str:
        """Convert any value to string for display."""
        if isinstance(v, str):
            return v
        return str(v)


class CrossReferenceReport(BaseModel):
    """Full cross-reference validation report."""

    issues: list[ValidationIssue] = Field(default_factory=list)
    warnings: list[ValidationIssue] = Field(default_factory=list)
    stats: dict[str, int] = Field(default_factory=dict)

    @property
    def is_valid(self) -> bool:
        return len(self.issues) == 0

    @property
    def has_warnings(self) -> bool:
        return len(self.warnings) > 0

    def add_issue(
        self,
        file: str,
        location: str,
        issue_type: str,
        message: str,
        value: str,
        suggestion: str | None = None,
    ) -> None:
        self.issues.append(
            ValidationIssue(
                file=file,
                location=location,
                issue_type=issue_type,  # type: ignore
                message=message,
                value=value,
                suggestion=suggestion,
            )
        )

    def add_warning(
        self,
        file: str,
        location: str,
        issue_type: str,
        message: str,
        value: str,
        suggestion: str | None = None,
    ) -> None:
        self.warnings.append(
            ValidationIssue(
                file=file,
                location=location,
                issue_type=issue_type,  # type: ignore
                message=message,
                value=value,
                suggestion=suggestion,
            )
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "valid": self.is_valid,
            "issue_count": len(self.issues),
            "warning_count": len(self.warnings),
            "issues": [i.model_dump() for i in self.issues],
            "warnings": [w.model_dump() for w in self.warnings],
            "stats": self.stats,
        }
