"""
Ideal State Schemas for createOS data quality validation.

These Pydantic schemas define the IDEAL state of createOS entities.
The gap between ideal and actual is the QualityDelta - which drives
coaching, auto-fill suggestions, and data quality scoring.

Philosophy:
    - Level 1 (Required): Fields that MUST exist for basic functionality
    - Level 2 (Recommended): Fields that SHOULD exist for good hygiene
    - Level 3 (Optimal): Fields that indicate excellence

The ideal state is not about perfection - it's about knowing the target
so we can measure distance and provide actionable coaching.

Usage:
    from lightwave.schema.pydantic.models.ideal_state import IdealTaskSchema

    task = Task.objects.get(id="...")
    delta = IdealTaskSchema.compute_delta(task)
    coaching = delta.generate_coaching()
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import date, datetime
from decimal import Decimal
from enum import StrEnum
from typing import TYPE_CHECKING, Any

from pydantic import Field

from lightwave.schema.pydantic.core.base import SSTSchema

if TYPE_CHECKING:
    pass


# =============================================================================
# VALIDATION LEVELS
# =============================================================================


class ValidationLevel(StrEnum):
    """Levels of field validation importance."""

    REQUIRED = "required"  # Must exist for basic functionality
    RECOMMENDED = "recommended"  # Should exist for good hygiene
    OPTIMAL = "optimal"  # Indicates excellence


class FieldSeverity(StrEnum):
    """Severity of a missing or invalid field."""

    CRITICAL = "critical"  # Blocks workflows
    WARNING = "warning"  # Degrades quality
    INFO = "info"  # Nice to have


# =============================================================================
# QUALITY DELTA STRUCTURES
# =============================================================================


@dataclass
class FieldGap:
    """A single field that differs from ideal state."""

    field_name: str
    level: ValidationLevel
    severity: FieldSeverity
    current_value: Any
    ideal_description: str
    auto_fillable: bool = False
    estimated_value: Any = None
    confidence: float = 0.0
    reason: str = ""

    def to_dict(self) -> dict[str, Any]:
        """Serialize to dict."""
        return {
            "field": self.field_name,
            "level": self.level.value,
            "severity": self.severity.value,
            "current": self.current_value,
            "ideal": self.ideal_description,
            "auto_fillable": self.auto_fillable,
            "estimated": self.estimated_value,
            "confidence": self.confidence,
            "reason": self.reason,
        }


@dataclass
class QualityDelta:
    """
    The delta between actual state and ideal state.

    This is the core data structure for data quality coaching.
    """

    entity_type: str
    entity_id: str
    entity_name: str
    computed_at: datetime = field(default_factory=datetime.now)

    # Field gaps by level
    required_gaps: list[FieldGap] = field(default_factory=list)
    recommended_gaps: list[FieldGap] = field(default_factory=list)
    optimal_gaps: list[FieldGap] = field(default_factory=list)

    # Constraint violations
    constraint_violations: list[str] = field(default_factory=list)

    # Relationship gaps
    relationship_gaps: list[str] = field(default_factory=list)

    @property
    def completeness_score(self) -> Decimal:
        """
        Calculate completeness score (0-100).

        Weights:
        - Required fields: 60% of score
        - Recommended fields: 30% of score
        - Optimal fields: 10% of score
        """
        # Count total possible fields at each level
        # (This is a simplified calculation)
        required_weight = 0.6
        recommended_weight = 0.3
        optimal_weight = 0.1

        # Assume 5 fields at each level for baseline
        required_complete = max(0, 5 - len(self.required_gaps)) / 5
        recommended_complete = max(0, 5 - len(self.recommended_gaps)) / 5
        optimal_complete = max(0, 5 - len(self.optimal_gaps)) / 5

        score = (
            required_complete * required_weight
            + recommended_complete * recommended_weight
            + optimal_complete * optimal_weight
        ) * 100

        return Decimal(str(round(score, 2)))

    @property
    def is_healthy(self) -> bool:
        """Return True if no critical gaps exist."""
        return len(self.required_gaps) == 0 and len(self.constraint_violations) == 0

    @property
    def all_gaps(self) -> list[FieldGap]:
        """Return all gaps sorted by severity."""
        severity_order = {
            FieldSeverity.CRITICAL: 0,
            FieldSeverity.WARNING: 1,
            FieldSeverity.INFO: 2,
        }
        all_gaps = self.required_gaps + self.recommended_gaps + self.optimal_gaps
        return sorted(all_gaps, key=lambda g: severity_order.get(g.severity, 3))

    @property
    def auto_fillable_gaps(self) -> list[FieldGap]:
        """Return gaps that can be auto-filled by AI."""
        return [g for g in self.all_gaps if g.auto_fillable and g.confidence >= 0.7]

    def to_dict(self) -> dict[str, Any]:
        """Serialize to dict."""
        return {
            "entity_type": self.entity_type,
            "entity_id": self.entity_id,
            "entity_name": self.entity_name,
            "computed_at": self.computed_at.isoformat(),
            "completeness_score": float(self.completeness_score),
            "is_healthy": self.is_healthy,
            "required_gaps": [g.to_dict() for g in self.required_gaps],
            "recommended_gaps": [g.to_dict() for g in self.recommended_gaps],
            "optimal_gaps": [g.to_dict() for g in self.optimal_gaps],
            "constraint_violations": self.constraint_violations,
            "relationship_gaps": self.relationship_gaps,
            "auto_fillable_count": len(self.auto_fillable_gaps),
        }


@dataclass
class CoachingAdvice:
    """
    Actionable coaching advice generated from QualityDelta.
    """

    priority: int  # 1 = highest
    category: str  # "missing_field", "constraint", "relationship", "pattern"
    suggestion: str
    reason: str
    auto_actionable: bool = False
    action_type: str = ""  # "fill", "link", "update", "create"
    target_field: str = ""
    estimated_value: Any = None
    confidence: float = 0.0

    def to_dict(self) -> dict[str, Any]:
        """Serialize to dict."""
        return {
            "priority": self.priority,
            "category": self.category,
            "suggestion": self.suggestion,
            "reason": self.reason,
            "auto_actionable": self.auto_actionable,
            "action_type": self.action_type,
            "target_field": self.target_field,
            "estimated_value": self.estimated_value,
            "confidence": self.confidence,
        }


# =============================================================================
# IDEAL STATE SCHEMAS
# =============================================================================


class IdealTaskSchema(SSTSchema):
    """
    Ideal state schema for a Task.

    Defines what a complete, well-formed task looks like.
    Used to compute QualityDelta for coaching.
    """

    # === Level 1: REQUIRED ===
    title: str = Field(
        ...,
        min_length=5,
        max_length=200,
        description="Clear, actionable task title",
    )
    status: str = Field(
        ...,
        min_length=1,
        description="Current task status",
    )

    # === Level 2: RECOMMENDED ===
    description: str | None = Field(
        None,
        min_length=20,
        description="Detailed description of what needs to be done",
    )
    acceptance_criteria: str | None = Field(
        None,
        min_length=10,
        description="Clear criteria for task completion",
    )
    priority: str | None = Field(
        None,
        description="Task priority level",
    )
    sprint_id: str | None = Field(
        None,
        description="Sprint this task belongs to",
    )
    epic_id: str | None = Field(
        None,
        description="Epic this task belongs to",
    )

    # === Level 3: OPTIMAL ===
    user_story_id: str | None = Field(
        None,
        description="User story this task implements",
    )
    branch_name: str | None = Field(
        None,
        description="Git branch for this task",
    )
    pr_url: str | None = Field(
        None,
        description="Pull request URL",
    )
    estimated_hours: float | None = Field(
        None,
        ge=0,
        description="Estimated hours to complete",
    )

    @classmethod
    def get_field_levels(cls) -> dict[str, ValidationLevel]:
        """Return mapping of field names to validation levels."""
        return {
            # Required
            "title": ValidationLevel.REQUIRED,
            "status": ValidationLevel.REQUIRED,
            # Recommended
            "description": ValidationLevel.RECOMMENDED,
            "acceptance_criteria": ValidationLevel.RECOMMENDED,
            "priority": ValidationLevel.RECOMMENDED,
            "sprint_id": ValidationLevel.RECOMMENDED,
            "epic_id": ValidationLevel.RECOMMENDED,
            # Optimal
            "user_story_id": ValidationLevel.OPTIMAL,
            "branch_name": ValidationLevel.OPTIMAL,
            "pr_url": ValidationLevel.OPTIMAL,
            "estimated_hours": ValidationLevel.OPTIMAL,
        }

    @classmethod
    def get_field_severities(cls) -> dict[str, FieldSeverity]:
        """Return mapping of field names to severities when missing."""
        return {
            "title": FieldSeverity.CRITICAL,
            "status": FieldSeverity.CRITICAL,
            "description": FieldSeverity.WARNING,
            "acceptance_criteria": FieldSeverity.WARNING,
            "priority": FieldSeverity.WARNING,
            "sprint_id": FieldSeverity.INFO,
            "epic_id": FieldSeverity.INFO,
            "user_story_id": FieldSeverity.INFO,
            "branch_name": FieldSeverity.INFO,
            "pr_url": FieldSeverity.INFO,
            "estimated_hours": FieldSeverity.INFO,
        }

    @classmethod
    def compute_delta(cls, task: Any) -> QualityDelta:
        """
        Compute QualityDelta for a task instance.

        Args:
            task: Django Task model instance

        Returns:
            QualityDelta with gaps and scores
        """
        delta = QualityDelta(
            entity_type="task",
            entity_id=str(task.id),
            entity_name=task.title,
        )

        field_levels = cls.get_field_levels()
        field_severities = cls.get_field_severities()

        # Check each field
        for field_name, level in field_levels.items():
            value = getattr(task, field_name, None)

            # Handle FK fields
            if field_name.endswith("_id"):
                base_field = field_name[:-3]  # Remove _id suffix
                fk_value = getattr(task, base_field, None)
                value = str(fk_value.id) if fk_value else None

            severity = field_severities.get(field_name, FieldSeverity.INFO)

            # Check if field is missing or inadequate
            is_gap = False
            reason = ""

            if value is None or value == "":
                is_gap = True
                reason = f"Field '{field_name}' is empty"
            elif field_name == "description" and len(str(value)) < 20:
                is_gap = True
                reason = f"Description too short ({len(str(value))} chars, need 20+)"
            elif field_name == "title" and len(str(value)) < 5:
                is_gap = True
                reason = f"Title too short ({len(str(value))} chars, need 5+)"

            if is_gap:
                gap = FieldGap(
                    field_name=field_name,
                    level=level,
                    severity=severity,
                    current_value=value,
                    ideal_description=cls._get_field_description(field_name),
                    auto_fillable=cls._is_auto_fillable(field_name),
                    reason=reason,
                )

                if level == ValidationLevel.REQUIRED:
                    delta.required_gaps.append(gap)
                elif level == ValidationLevel.RECOMMENDED:
                    delta.recommended_gaps.append(gap)
                else:
                    delta.optimal_gaps.append(gap)

        # Check constraints
        delta.constraint_violations.extend(cls._check_constraints(task))

        # Check relationships
        delta.relationship_gaps.extend(cls._check_relationships(task))

        return delta

    @classmethod
    def _get_field_description(cls, field_name: str) -> str:
        """Get ideal description for a field."""
        descriptions = {
            "title": "Clear, actionable title (5-200 chars)",
            "status": "Valid task status",
            "description": "Detailed description (20+ chars)",
            "acceptance_criteria": "Clear completion criteria",
            "priority": "Priority level (P0-P3)",
            "sprint_id": "Assigned to active sprint",
            "epic_id": "Linked to parent epic",
            "user_story_id": "Linked to user story",
            "branch_name": "Git branch name",
            "pr_url": "Pull request URL",
            "estimated_hours": "Time estimate in hours",
        }
        return descriptions.get(field_name, f"Valid {field_name}")

    @classmethod
    def _is_auto_fillable(cls, field_name: str) -> bool:
        """Check if a field can be auto-filled by AI."""
        auto_fillable = {
            "description",
            "acceptance_criteria",
            "priority",
            "estimated_hours",
            "branch_name",
        }
        return field_name in auto_fillable

    @classmethod
    def _check_constraints(cls, task: Any) -> list[str]:
        """Check cross-field constraints."""
        violations = []

        # In-progress tasks should have a branch
        if task.status == "in_progress" and not task.branch_name:
            violations.append("In-progress tasks should have a branch_name")

        # Completed tasks should have a PR
        if task.status in ("in_review", "archived") and not task.pr_url:
            violations.append("Completed tasks should have a pr_url")

        return violations

    @classmethod
    def _check_relationships(cls, task: Any) -> list[str]:
        """Check relationship requirements."""
        gaps = []

        # Tasks with high story points should have implementation plans
        if hasattr(task, "user_story") and task.user_story:
            if hasattr(task.user_story, "story_points"):
                if task.user_story.story_points and task.user_story.story_points >= 8:
                    # Check if plan exists
                    from django.apps import apps

                    try:
                        ImplementationPlan = apps.get_model("createos", "ImplementationPlan")
                        if not ImplementationPlan.objects.filter(task=task).exists():
                            gaps.append(
                                f"Task with {task.user_story.story_points} story points should have an implementation plan"
                            )
                    except LookupError:
                        pass

        return gaps


class IdealSprintSchema(SSTSchema):
    """
    Ideal state schema for a Sprint.

    Defines what a complete, well-formed sprint looks like.
    """

    # === Level 1: REQUIRED ===
    name: str = Field(
        ...,
        min_length=3,
        max_length=100,
        description="Sprint name",
    )
    status: str = Field(
        ...,
        description="Sprint status",
    )
    start_date: date | None = Field(
        ...,
        description="Sprint start date",
    )
    end_date: date | None = Field(
        ...,
        description="Sprint end date",
    )

    # === Level 2: RECOMMENDED ===
    objectives: str | None = Field(
        None,
        min_length=10,
        description="Sprint objectives/goals",
    )
    epic_id: str | None = Field(
        None,
        description="Parent epic",
    )

    # === Level 3: OPTIMAL ===
    quality_score: Decimal | None = Field(
        None,
        ge=0,
        le=100,
        description="Sprint quality score",
    )

    @classmethod
    def get_field_levels(cls) -> dict[str, ValidationLevel]:
        """Return mapping of field names to validation levels."""
        return {
            "name": ValidationLevel.REQUIRED,
            "status": ValidationLevel.REQUIRED,
            "start_date": ValidationLevel.REQUIRED,
            "end_date": ValidationLevel.REQUIRED,
            "objectives": ValidationLevel.RECOMMENDED,
            "epic_id": ValidationLevel.RECOMMENDED,
            "quality_score": ValidationLevel.OPTIMAL,
        }

    @classmethod
    def compute_delta(cls, sprint: Any) -> QualityDelta:
        """Compute QualityDelta for a sprint instance."""
        delta = QualityDelta(
            entity_type="sprint",
            entity_id=str(sprint.id),
            entity_name=sprint.name,
        )

        field_levels = cls.get_field_levels()

        for field_name, level in field_levels.items():
            value = getattr(sprint, field_name, None)

            if field_name.endswith("_id"):
                base_field = field_name[:-3]
                fk_value = getattr(sprint, base_field, None)
                value = str(fk_value.id) if fk_value else None

            if value is None or value == "":
                gap = FieldGap(
                    field_name=field_name,
                    level=level,
                    severity=FieldSeverity.WARNING if level == ValidationLevel.REQUIRED else FieldSeverity.INFO,
                    current_value=value,
                    ideal_description=f"Valid {field_name}",
                    auto_fillable=field_name == "objectives",
                    reason=f"Field '{field_name}' is empty",
                )

                if level == ValidationLevel.REQUIRED:
                    delta.required_gaps.append(gap)
                elif level == ValidationLevel.RECOMMENDED:
                    delta.recommended_gaps.append(gap)
                else:
                    delta.optimal_gaps.append(gap)

        # Constraint: end_date should be after start_date
        if sprint.start_date and sprint.end_date:
            if sprint.end_date < sprint.start_date:
                delta.constraint_violations.append("end_date must be after start_date")

        return delta


class IdealDocumentSchema(SSTSchema):
    """
    Ideal state schema for a Document.

    Defines what a complete, well-formed document looks like.
    """

    # === Level 1: REQUIRED ===
    name: str = Field(
        ...,
        min_length=3,
        max_length=200,
        description="Document name",
    )
    doc_type: str = Field(
        ...,
        description="Document type",
    )
    status: str = Field(
        ...,
        description="Document status",
    )

    # === Level 2: RECOMMENDED ===
    content: str | None = Field(
        None,
        min_length=50,
        description="Document content (markdown)",
    )
    vertical: str | None = Field(
        None,
        description="Vertical this document belongs to",
    )

    # === Level 3: OPTIMAL ===
    tags: list[str] = Field(
        default_factory=list,
        description="Tags for categorization",
    )
    linked_entities: int = Field(
        0,
        ge=0,
        description="Number of linked entities",
    )

    @classmethod
    def get_field_levels(cls) -> dict[str, ValidationLevel]:
        """Return mapping of field names to validation levels."""
        return {
            "name": ValidationLevel.REQUIRED,
            "doc_type": ValidationLevel.REQUIRED,
            "status": ValidationLevel.REQUIRED,
            "content": ValidationLevel.RECOMMENDED,
            "vertical": ValidationLevel.RECOMMENDED,
            "tags": ValidationLevel.OPTIMAL,
            "linked_entities": ValidationLevel.OPTIMAL,
        }

    @classmethod
    def compute_delta(cls, document: Any) -> QualityDelta:
        """Compute QualityDelta for a document instance."""
        delta = QualityDelta(
            entity_type="document",
            entity_id=str(document.id),
            entity_name=document.name,
        )

        field_levels = cls.get_field_levels()

        for field_name, level in field_levels.items():
            if field_name == "linked_entities":
                # Special handling: count KnowledgeLinks
                try:
                    from django.apps import apps

                    KnowledgeLink = apps.get_model("createos", "KnowledgeLink")
                    count = KnowledgeLink.objects.filter(
                        source_type="document",
                        source_id=document.id,
                    ).count()
                    if count == 0:
                        gap = FieldGap(
                            field_name=field_name,
                            level=level,
                            severity=FieldSeverity.INFO,
                            current_value=0,
                            ideal_description="At least 1 linked entity",
                            auto_fillable=True,
                            reason="Document has no links (isolated knowledge)",
                        )
                        delta.optimal_gaps.append(gap)
                except LookupError:
                    pass
                continue

            if field_name == "tags":
                tags = getattr(document, "tags", None) or []
                if len(tags) == 0:
                    gap = FieldGap(
                        field_name=field_name,
                        level=level,
                        severity=FieldSeverity.INFO,
                        current_value=tags,
                        ideal_description="At least 1 tag for categorization",
                        auto_fillable=True,
                        reason="No tags assigned",
                    )
                    delta.optimal_gaps.append(gap)
                continue

            value = getattr(document, field_name, None)

            is_gap = False
            reason = ""

            if value is None or value == "":
                is_gap = True
                reason = f"Field '{field_name}' is empty"
            elif field_name == "content" and len(str(value)) < 50:
                is_gap = True
                reason = f"Content too short ({len(str(value))} chars, need 50+)"

            if is_gap:
                gap = FieldGap(
                    field_name=field_name,
                    level=level,
                    severity=FieldSeverity.WARNING if level == ValidationLevel.REQUIRED else FieldSeverity.INFO,
                    current_value=value,
                    ideal_description=f"Valid {field_name}",
                    auto_fillable=field_name in ("content", "tags"),
                    reason=reason,
                )

                if level == ValidationLevel.REQUIRED:
                    delta.required_gaps.append(gap)
                elif level == ValidationLevel.RECOMMENDED:
                    delta.recommended_gaps.append(gap)
                else:
                    delta.optimal_gaps.append(gap)

        return delta


# =============================================================================
# COACHING GENERATOR
# =============================================================================


def generate_coaching(delta: QualityDelta, max_suggestions: int = 5) -> list[CoachingAdvice]:
    """
    Generate prioritized coaching advice from a QualityDelta.

    Args:
        delta: QualityDelta computed from ideal state comparison
        max_suggestions: Maximum number of suggestions to return

    Returns:
        List of CoachingAdvice sorted by priority
    """
    advice_list: list[CoachingAdvice] = []
    priority = 1

    # Priority 1: Critical required gaps
    for gap in delta.required_gaps:
        if gap.severity == FieldSeverity.CRITICAL:
            advice_list.append(
                CoachingAdvice(
                    priority=priority,
                    category="missing_field",
                    suggestion=f"Add {gap.field_name}: {gap.ideal_description}",
                    reason=gap.reason,
                    auto_actionable=gap.auto_fillable,
                    action_type="fill" if gap.auto_fillable else "manual",
                    target_field=gap.field_name,
                    estimated_value=gap.estimated_value,
                    confidence=gap.confidence,
                )
            )
            priority += 1

    # Priority 2: Constraint violations
    for violation in delta.constraint_violations:
        advice_list.append(
            CoachingAdvice(
                priority=priority,
                category="constraint",
                suggestion=f"Fix constraint: {violation}",
                reason="Data integrity issue",
                auto_actionable=False,
                action_type="update",
            )
        )
        priority += 1

    # Priority 3: Recommended gaps
    for gap in delta.recommended_gaps:
        advice_list.append(
            CoachingAdvice(
                priority=priority,
                category="missing_field",
                suggestion=f"Consider adding {gap.field_name}: {gap.ideal_description}",
                reason=gap.reason,
                auto_actionable=gap.auto_fillable,
                action_type="fill" if gap.auto_fillable else "manual",
                target_field=gap.field_name,
                estimated_value=gap.estimated_value,
                confidence=gap.confidence,
            )
        )
        priority += 1

    # Priority 4: Relationship gaps
    for gap in delta.relationship_gaps:
        advice_list.append(
            CoachingAdvice(
                priority=priority,
                category="relationship",
                suggestion=gap,
                reason="Missing relationship for complex work",
                auto_actionable=False,
                action_type="create",
            )
        )
        priority += 1

    # Priority 5: Optimal gaps (only if space)
    for gap in delta.optimal_gaps:
        if len(advice_list) < max_suggestions:
            advice_list.append(
                CoachingAdvice(
                    priority=priority,
                    category="enhancement",
                    suggestion=f"Optional: Add {gap.field_name} for completeness",
                    reason=gap.reason,
                    auto_actionable=gap.auto_fillable,
                    action_type="fill" if gap.auto_fillable else "manual",
                    target_field=gap.field_name,
                    estimated_value=gap.estimated_value,
                    confidence=gap.confidence,
                )
            )
            priority += 1

    return advice_list[:max_suggestions]
