"""
Reconciliation Engine - Sync database state to match YAML ideal state.

This module provides the core reconciliation logic for comparing YAML
definitions against database reality and applying necessary changes.

Philosophy:
    YAML files define WHAT SHOULD EXIST, not what currently exists.
    The reconciler ensures reality matches the YAML ideal state.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any

from lightwave.schema.loaders.base import SchemaLoader

logger = logging.getLogger(__name__)


class DriftStatus(str, Enum):
    """Status of an item in drift detection."""

    IN_SYNC = "in_sync"
    MISSING = "missing"  # In YAML but not in DB
    DRIFT = "drift"  # In both but values differ
    ORPHAN = "orphan"  # In DB but not in YAML
    ERROR = "error"  # Comparison failed


@dataclass
class DriftItem:
    """A single item that differs between YAML and database."""

    status: DriftStatus
    model: str
    key: str | tuple[str, ...]
    yaml_value: dict[str, Any] | None = None
    db_value: dict[str, Any] | None = None
    diff_fields: list[str] = field(default_factory=list)
    error: str | None = None

    def __str__(self) -> str:
        if self.status == DriftStatus.MISSING:
            return f"MISSING: {self.model} '{self.key}' (defined in YAML, not in DB)"
        elif self.status == DriftStatus.DRIFT:
            fields = ", ".join(self.diff_fields)
            return f"DRIFT: {self.model} '{self.key}' differs on: {fields}"
        elif self.status == DriftStatus.ORPHAN:
            return f"ORPHAN: {self.model} '{self.key}' (in DB but not in YAML)"
        elif self.status == DriftStatus.ERROR:
            return f"ERROR: {self.model} '{self.key}' - {self.error}"
        else:
            return f"IN_SYNC: {self.model} '{self.key}'"


@dataclass
class DriftReport:
    """
    Report of differences between YAML ideal state and database reality.
    """

    schema_name: str
    generated_at: datetime = field(default_factory=datetime.now)
    items: list[DriftItem] = field(default_factory=list)

    @property
    def total_items(self) -> int:
        return len(self.items)

    @property
    def in_sync_count(self) -> int:
        return len([i for i in self.items if i.status == DriftStatus.IN_SYNC])

    @property
    def missing_count(self) -> int:
        return len([i for i in self.items if i.status == DriftStatus.MISSING])

    @property
    def drift_count(self) -> int:
        return len([i for i in self.items if i.status == DriftStatus.DRIFT])

    @property
    def orphan_count(self) -> int:
        return len([i for i in self.items if i.status == DriftStatus.ORPHAN])

    @property
    def error_count(self) -> int:
        return len([i for i in self.items if i.status == DriftStatus.ERROR])

    @property
    def is_clean(self) -> bool:
        """Return True if everything is in sync."""
        return all(i.status == DriftStatus.IN_SYNC for i in self.items)

    @property
    def needs_attention(self) -> list[DriftItem]:
        """Return items that need action."""
        return [i for i in self.items if i.status != DriftStatus.IN_SYNC]

    def add_in_sync(self, model: str, key: str | tuple[str, ...]) -> None:
        self.items.append(DriftItem(status=DriftStatus.IN_SYNC, model=model, key=key))

    def add_missing(self, model: str, key: str | tuple[str, ...], yaml_value: dict[str, Any]) -> None:
        self.items.append(DriftItem(status=DriftStatus.MISSING, model=model, key=key, yaml_value=yaml_value))

    def add_drift(
        self,
        model: str,
        key: str | tuple[str, ...],
        yaml_value: dict[str, Any],
        db_value: dict[str, Any],
        diff_fields: list[str],
    ) -> None:
        self.items.append(
            DriftItem(
                status=DriftStatus.DRIFT,
                model=model,
                key=key,
                yaml_value=yaml_value,
                db_value=db_value,
                diff_fields=diff_fields,
            )
        )

    def add_orphan(self, model: str, key: str | tuple[str, ...], db_value: dict[str, Any]) -> None:
        self.items.append(DriftItem(status=DriftStatus.ORPHAN, model=model, key=key, db_value=db_value))

    def add_error(self, model: str, key: str | tuple[str, ...], error: str) -> None:
        self.items.append(DriftItem(status=DriftStatus.ERROR, model=model, key=key, error=error))

    def summary(self) -> str:
        """Generate a human-readable summary."""
        lines = [
            f"DRIFT REPORT: {self.schema_name}",
            f"Generated: {self.generated_at.isoformat()}",
            "-" * 50,
            f"Total items: {self.total_items}",
            f"  ✅ In sync: {self.in_sync_count}",
            f"  ⚠️  Missing: {self.missing_count}",
            f"  ⚠️  Drift: {self.drift_count}",
            f"  ❓ Orphan: {self.orphan_count}",
            f"  ❌ Error: {self.error_count}",
        ]

        if self.is_clean:
            lines.append("\n✅ All items in sync!")
        else:
            lines.append("\nItems needing attention:")
            for item in self.needs_attention:
                lines.append(f"  - {item}")

        return "\n".join(lines)

    def to_dict(self) -> dict[str, Any]:
        """Convert report to dictionary for JSON serialization."""
        return {
            "schema_name": self.schema_name,
            "generated_at": self.generated_at.isoformat(),
            "summary": {
                "total": self.total_items,
                "in_sync": self.in_sync_count,
                "missing": self.missing_count,
                "drift": self.drift_count,
                "orphan": self.orphan_count,
                "error": self.error_count,
            },
            "is_clean": self.is_clean,
            "items": [
                {
                    "status": item.status.value,
                    "model": item.model,
                    "key": item.key,
                    "diff_fields": item.diff_fields,
                    "error": item.error,
                }
                for item in self.items
            ],
        }


class Reconciler:
    """
    Reconcile database state to match YAML ideal state.

    Usage:
        reconciler = Reconciler()
        report = reconciler.check('layouts')  # Dry run
        report = reconciler.apply('layouts')  # Apply changes
    """

    def __init__(self, loader: SchemaLoader | None = None):
        """
        Initialize the reconciler.

        Args:
            loader: Optional custom SchemaLoader instance
        """
        self.loader = loader or SchemaLoader()

    def check(self, schema_name: str) -> DriftReport:
        """
        Check for drift without making changes.

        Args:
            schema_name: Name of the schema to check

        Returns:
            DriftReport with current state
        """
        return self._reconcile(schema_name, apply_changes=False)

    def apply(self, schema_name: str) -> DriftReport:
        """
        Apply changes to match YAML ideal state.

        Args:
            schema_name: Name of the schema to reconcile

        Returns:
            DriftReport with changes made
        """
        return self._reconcile(schema_name, apply_changes=True)

    def _reconcile(self, schema_name: str, apply_changes: bool) -> DriftReport:
        """
        Internal reconciliation logic.

        Args:
            schema_name: Name of the schema
            apply_changes: Whether to apply changes or just report

        Returns:
            DriftReport
        """
        report = DriftReport(schema_name=schema_name)

        try:
            # Get model info
            app_label, model_name, key_field = self.loader.get_model_info(schema_name)
        except ValueError as e:
            report.add_error("unknown", "unknown", str(e))
            return report

        try:
            # Import Django model
            from django.apps import apps

            model = apps.get_model(app_label, model_name)
        except (LookupError, ImportError) as e:
            report.add_error(f"{app_label}.{model_name}", "model", f"Cannot load model: {e}")
            return report

        # Load YAML items
        yaml_items = self.loader.get_items(schema_name)
        yaml_by_key = {}

        for item in yaml_items:
            key = self._extract_key(item, key_field)
            if key:
                yaml_by_key[key] = item

        # Load DB items
        db_items = model.objects.all()
        db_by_key = {}

        for obj in db_items:
            key = self._extract_key_from_obj(obj, key_field)
            if key:
                db_by_key[key] = obj

        # Compare YAML vs DB
        for key, yaml_item in yaml_by_key.items():
            db_obj = db_by_key.get(key)

            if db_obj is None:
                # Missing in DB
                report.add_missing(f"{app_label}.{model_name}", key, yaml_item)
                if apply_changes:
                    self._create_item(model, yaml_item, key_field, schema_name)
            else:
                # Check for drift
                diff_fields = self._compare_item(yaml_item, db_obj, schema_name)
                if diff_fields:
                    db_dict = self._obj_to_dict(db_obj, yaml_item.keys())
                    report.add_drift(f"{app_label}.{model_name}", key, yaml_item, db_dict, diff_fields)
                    if apply_changes:
                        self._update_item(db_obj, yaml_item, diff_fields, schema_name)
                else:
                    report.add_in_sync(f"{app_label}.{model_name}", key)

        # Check for orphans (in DB but not in YAML)
        for key, db_obj in db_by_key.items():
            if key not in yaml_by_key:
                db_dict = self._obj_to_dict(db_obj, ["name", "description"])
                report.add_orphan(f"{app_label}.{model_name}", key, db_dict)

        return report

    def _extract_key(self, item: dict[str, Any], key_field: str | tuple[str, ...]) -> str | tuple[str, ...] | None:
        """Extract key value from YAML item."""
        if isinstance(key_field, str):
            return item.get(key_field)
        else:
            # Composite key
            values = []
            for field_name in key_field:
                # Handle nested fields like site__domain
                if "__" in field_name:
                    parts = field_name.split("__")
                    value = item
                    for part in parts:
                        if isinstance(value, dict):
                            value = value.get(part)
                        else:
                            value = None
                            break
                else:
                    value = item.get(field_name)
                values.append(value)
            return tuple(values) if all(v is not None for v in values) else None

    def _extract_key_from_obj(self, obj: Any, key_field: str | tuple[str, ...]) -> str | tuple[str, ...] | None:
        """Extract key value from Django model instance."""
        if isinstance(key_field, str):
            return getattr(obj, key_field, None)
        else:
            values = []
            for field_name in key_field:
                if "__" in field_name:
                    # Handle FK traversal
                    parts = field_name.split("__")
                    value = obj
                    for part in parts:
                        value = getattr(value, part, None)
                        if value is None:
                            break
                else:
                    value = getattr(obj, field_name, None)
                values.append(value)
            return tuple(values) if all(v is not None for v in values) else None

    def _compare_item(self, yaml_item: dict[str, Any], db_obj: Any, schema_name: str) -> list[str]:
        """
        Compare YAML item to DB object and return list of differing fields.

        Args:
            yaml_item: Item from YAML
            db_obj: Django model instance
            schema_name: Name of schema (for field filtering)

        Returns:
            List of field names that differ
        """
        diff_fields = []

        # Fields to compare based on schema
        compare_fields = self._get_compare_fields(schema_name)

        for field_name in compare_fields:
            yaml_value = yaml_item.get(field_name)
            db_value = getattr(db_obj, field_name, None)

            # Skip None/missing values
            if yaml_value is None:
                continue

            # Normalize for comparison
            if isinstance(db_value, bool) and isinstance(yaml_value, bool):
                if yaml_value != db_value:
                    diff_fields.append(field_name)
            elif isinstance(db_value, (dict, list)):
                # JSON fields
                if yaml_value != db_value:
                    diff_fields.append(field_name)
            elif str(yaml_value) != str(db_value):
                diff_fields.append(field_name)

        return diff_fields

    def _get_compare_fields(self, schema_name: str) -> list[str]:
        """Get list of fields to compare for a schema."""
        field_map = {
            "layouts": ["name", "template_path", "description", "is_active"],
            "blueprints": [
                "name",
                "display_name",
                "description",
                "schema",
                "default_islands",
                "is_locked",
                "is_active",
            ],
            "pages": ["path", "title", "page_type", "blueprint", "layout", "data"],
            "routes": ["pattern", "name", "requires"],
            "navigation": ["label", "url", "order", "is_active"],
            "features": ["slug", "features"],
        }
        return field_map.get(schema_name, [])

    def _obj_to_dict(self, obj: Any, fields: list[str] | set[str]) -> dict[str, Any]:
        """Convert Django model instance to dictionary."""
        result = {}
        for field_name in fields:
            value = getattr(obj, field_name, None)
            if hasattr(value, "pk"):
                # FK - get string representation
                value = str(value)
            result[field_name] = value
        return result

    def _create_item(
        self,
        model: Any,
        yaml_item: dict[str, Any],
        key_field: str | tuple[str, ...],
        schema_name: str,
    ) -> Any:
        """Create a new database item from YAML definition."""
        # Filter to model fields only
        model_fields = {f.name for f in model._meta.get_fields()}
        create_data = {k: v for k, v in yaml_item.items() if k in model_fields}

        # Handle special fields
        if schema_name == "layouts":
            # layouts.yaml uses lowercase names that match LayoutType choices
            pass

        if schema_name == "blueprints":
            # Handle allowed_layouts M2M
            create_data.pop("allowed_layouts", None)
            create_data.pop("default_layout", None)

        try:
            obj = model.objects.create(**create_data)
            logger.info(f"Created {model.__name__}: {obj}")
            return obj
        except Exception as e:
            logger.error(f"Failed to create {model.__name__}: {e}")
            raise

    def _update_item(
        self,
        db_obj: Any,
        yaml_item: dict[str, Any],
        diff_fields: list[str],
        schema_name: str,
    ) -> None:
        """Update existing database item to match YAML."""
        for field_name in diff_fields:
            yaml_value = yaml_item.get(field_name)
            if yaml_value is not None:
                setattr(db_obj, field_name, yaml_value)

        try:
            db_obj.save(update_fields=diff_fields)
            logger.info(f"Updated {db_obj.__class__.__name__}: {db_obj}")
        except Exception as e:
            logger.error(f"Failed to update {db_obj}: {e}")
            raise


def reconcile(schema_name: str, check_only: bool = False, loader: SchemaLoader | None = None) -> DriftReport:
    """
    Reconcile a schema's database state to match YAML ideal state.

    Convenience function using the default reconciler.

    Args:
        schema_name: Name of the schema to reconcile
        check_only: If True, only check for drift without applying changes
        loader: Optional custom SchemaLoader

    Returns:
        DriftReport with results
    """
    reconciler = Reconciler(loader=loader)
    if check_only:
        return reconciler.check(schema_name)
    else:
        return reconciler.apply(schema_name)


def reconcile_all(check_only: bool = False, loader: SchemaLoader | None = None) -> dict[str, DriftReport]:
    """
    Reconcile all schemas.

    Args:
        check_only: If True, only check for drift without applying changes
        loader: Optional custom SchemaLoader

    Returns:
        Dictionary mapping schema names to their DriftReports
    """
    from lightwave.schema.loaders.base import SCHEMA_MODEL_MAP

    reconciler = Reconciler(loader=loader)
    reports = {}

    for schema_name in SCHEMA_MODEL_MAP:
        try:
            if check_only:
                reports[schema_name] = reconciler.check(schema_name)
            else:
                reports[schema_name] = reconciler.apply(schema_name)
        except Exception as e:
            logger.error(f"Failed to reconcile {schema_name}: {e}")
            report = DriftReport(schema_name=schema_name)
            report.add_error("reconciliation", schema_name, str(e))
            reports[schema_name] = report

    return reports
