"""Pydantic models for the job queue."""

from __future__ import annotations

import json
from datetime import datetime
from enum import Enum
from typing import Any

from pydantic import BaseModel, Field, field_validator


class JobStatus(str, Enum):
    """Job status enumeration."""

    PENDING = "pending"
    RUNNING = "running"
    PAUSED = "paused"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"


class JobPriority(int, Enum):
    """Job priority enumeration.

    Higher values indicate higher priority.
    CRITICAL jobs can preempt lower priority running jobs.
    """

    CRITICAL = 100  # Can preempt running jobs
    HIGH = 50
    NORMAL = 0
    LOW = -50

    def __gt__(self, other: object) -> bool:
        if isinstance(other, JobPriority):
            return self.value > other.value
        if isinstance(other, int):
            return self.value > other
        return NotImplemented

    def __ge__(self, other: object) -> bool:
        if isinstance(other, JobPriority):
            return self.value >= other.value
        if isinstance(other, int):
            return self.value >= other
        return NotImplemented

    def __lt__(self, other: object) -> bool:
        if isinstance(other, JobPriority):
            return self.value < other.value
        if isinstance(other, int):
            return self.value < other
        return NotImplemented

    def __le__(self, other: object) -> bool:
        if isinstance(other, JobPriority):
            return self.value <= other.value
        if isinstance(other, int):
            return self.value <= other
        return NotImplemented


def _int_to_priority(value: int) -> JobPriority:
    """Convert integer value to JobPriority enum."""
    # Check for exact match first
    for priority in JobPriority:
        if priority.value == value:
            return priority
    # Find closest priority for values in between
    if value >= 75:
        return JobPriority.CRITICAL
    elif value >= 25:
        return JobPriority.HIGH
    elif value >= -25:
        return JobPriority.NORMAL
    else:
        return JobPriority.LOW


class Job(BaseModel):
    """Represents a job in the queue.

    Attributes:
        id: Unique identifier for the job
        payload: The job data/payload (stored as JSON)
        status: Current status of the job
        created_at: When the job was created
        updated_at: When the job was last updated
        scheduled_at: When the job should run (for delayed execution)
        started_at: When the job started running
        completed_at: When the job completed/failed
        retry_count: Number of retry attempts
        max_retries: Maximum number of retries allowed
        error: Error message if job failed
        priority: Job priority (higher = more important)
        depends_on: List of job IDs that must complete before this job runs
        job_type: Type of job (for encryption opt-in)
        encrypted: Whether the payload is encrypted
        encryption_metadata: Metadata about encryption if encrypted
        cron_expression: Cron expression for recurring jobs (optional)
        timezone: Timezone for cron scheduling (default UTC)
        next_run_at: Calculated next run time for cron jobs
        timeout_seconds: Timeout for job execution (None = no timeout)
        cancellation_reason: Reason for cancellation if status is CANCELLED
        cancelled_at: When the job was cancelled
        cancelled_by: User/process that cancelled the job
        tenant_id: Optional tenant identifier for multi-tenant support
    """

    id: int | None = Field(default=None, description="Unique job identifier")
    payload: dict[str, Any] = Field(default_factory=dict, description="Job payload data")
    status: JobStatus = Field(default=JobStatus.PENDING, description="Current job status")
    created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation timestamp")
    updated_at: datetime = Field(
        default_factory=datetime.utcnow, description="Last update timestamp"
    )
    scheduled_at: datetime | None = Field(default=None, description="Scheduled run time")
    started_at: datetime | None = Field(default=None, description="When job started running")
    completed_at: datetime | None = Field(default=None, description="When job completed")
    retry_count: int = Field(default=0, ge=0, description="Number of retries attempted")
    max_retries: int = Field(default=3, ge=0, description="Maximum retry attempts")
    error: str | None = Field(default=None, description="Error message if failed")
    priority: JobPriority = Field(default=JobPriority.NORMAL, description="Job priority")
    depends_on: list[int] | None = Field(default=None, description="Job IDs this job depends on")
    job_type: str = Field(default="default", description="Job type for encryption opt-in")
    encrypted: bool = Field(default=False, description="Whether payload is encrypted")
    encryption_metadata: dict[str, Any] | None = Field(
        default=None, description="Encryption metadata"
    )
    cron_expression: str | None = Field(
        default=None, description="Cron expression for recurring jobs"
    )
    timezone: str = Field(default="UTC", description="Timezone for cron scheduling")
    next_run_at: datetime | None = Field(
        default=None, description="Next scheduled run time for cron jobs"
    )
    timeout_seconds: float | None = Field(default=None, description="Job timeout in seconds")
    cancellation_reason: str | None = Field(default=None, description="Reason for cancellation")
    cancelled_at: datetime | None = Field(default=None, description="When job was cancelled")
    cancelled_by: str | None = Field(default=None, description="Who cancelled the job")
    tenant_id: str | None = Field(
        default=None, description="Tenant identifier for multi-tenant support"
    )

    @field_validator("priority", mode="before")
    @classmethod
    def validate_priority(cls, v):
        """Convert int to JobPriority enum."""
        if isinstance(v, int) and not isinstance(v, JobPriority):
            return _int_to_priority(v)
        return v

    def model_dump_json(self, **kwargs: Any) -> str:
        """Serialize job to JSON string."""
        data = self.model_dump(mode="json")
        return json.dumps(data)

    @classmethod
    def from_json(cls, json_str: str) -> Job:
        """Create Job from JSON string."""
        data = json.loads(json_str)
        return cls.model_validate(data)

    def is_retryable(self) -> bool:
        """Check if job can be retried."""
        return self.retry_count < self.max_retries and self.status == JobStatus.FAILED

    def should_run(self) -> bool:
        """Check if job is ready to run (pending and scheduled time reached)."""
        if self.status != JobStatus.PENDING:
            return False
        if self.scheduled_at is None:
            return True
        return datetime.utcnow() >= self.scheduled_at

    def to_db_dict(self) -> dict[str, Any]:
        """Convert to dictionary suitable for database storage.

        Uses SQLite-compatible datetime format (no T separator, no microseconds)
        to ensure proper lexicographic comparison with datetime('now').
        """

        def fmt_dt(dt: datetime | None) -> str | None:
            if dt is None:
                return None
            # Format: "YYYY-MM-DD HH:MM:SS" - compatible with SQLite datetime()
            return dt.strftime("%Y-%m-%d %H:%M:%S")

        return {
            "id": self.id,
            "payload": json.dumps(self.payload),
            "status": self.status.value,
            "created_at": fmt_dt(self.created_at),
            "updated_at": fmt_dt(self.updated_at),
            "scheduled_at": fmt_dt(self.scheduled_at),
            "started_at": fmt_dt(self.started_at),
            "completed_at": fmt_dt(self.completed_at),
            "retry_count": self.retry_count,
            "max_retries": self.max_retries,
            "error": self.error,
            "priority": self.priority.value,
            "depends_on": json.dumps(self.depends_on) if self.depends_on else None,
            "job_type": self.job_type,
            "encrypted": self.encrypted,
            "encryption_metadata": (
                json.dumps(self.encryption_metadata) if self.encryption_metadata else None
            ),
            "cron_expression": self.cron_expression,
            "timezone": self.timezone,
            "next_run_at": fmt_dt(self.next_run_at),
            "timeout_seconds": self.timeout_seconds,
            "cancellation_reason": self.cancellation_reason,
            "cancelled_at": fmt_dt(self.cancelled_at),
            "cancelled_by": self.cancelled_by,
            "tenant_id": self.tenant_id,
        }

    @classmethod
    def from_db_row(cls, row: dict[str, Any]) -> Job:
        """Create Job from database row dictionary."""
        # Parse priority - could be int or JobPriority
        priority_val = row.get("priority", 0)
        if isinstance(priority_val, int):
            # Use module-level function to convert
            priority = _int_to_priority(priority_val)
        else:
            priority = JobPriority.NORMAL

        # Parse depends_on
        depends_on = None
        if row.get("depends_on"):
            try:
                depends_on = json.loads(row["depends_on"])
            except (json.JSONDecodeError, TypeError):
                depends_on = None

        # Parse encryption_metadata
        encryption_metadata = None
        if row.get("encryption_metadata"):
            try:
                encryption_metadata = json.loads(row["encryption_metadata"])
            except (json.JSONDecodeError, TypeError):
                encryption_metadata = None

        # Parse cancelled_at
        cancelled_at = None
        if row.get("cancelled_at"):
            try:
                cancelled_at = datetime.fromisoformat(row["cancelled_at"])
            except (ValueError, TypeError):
                cancelled_at = None

        return cls(
            id=row["id"],
            payload=json.loads(row["payload"]),
            status=JobStatus(row["status"]),
            created_at=datetime.fromisoformat(row["created_at"]),
            updated_at=datetime.fromisoformat(row["updated_at"]),
            scheduled_at=(
                datetime.fromisoformat(row["scheduled_at"]) if row["scheduled_at"] else None
            ),
            started_at=datetime.fromisoformat(row["started_at"]) if row["started_at"] else None,
            completed_at=(
                datetime.fromisoformat(row["completed_at"]) if row["completed_at"] else None
            ),
            retry_count=row["retry_count"],
            max_retries=row["max_retries"],
            error=row["error"],
            priority=priority,
            depends_on=depends_on,
            job_type=row.get("job_type", "default"),
            encrypted=bool(row.get("encrypted", False)),
            encryption_metadata=encryption_metadata,
            cron_expression=row.get("cron_expression"),
            timezone=row.get("timezone", "UTC"),
            next_run_at=(
                datetime.fromisoformat(row["next_run_at"]) if row.get("next_run_at") else None
            ),
            timeout_seconds=row.get("timeout_seconds"),
            cancellation_reason=row.get("cancellation_reason"),
            cancelled_at=cancelled_at,
            cancelled_by=row.get("cancelled_by"),
            tenant_id=row.get("tenant_id"),
        )


class JobStats(BaseModel):
    """Statistics about the job queue."""

    total_jobs: int = Field(default=0, description="Total number of jobs")
    pending: int = Field(default=0, description="Number of pending jobs")
    running: int = Field(default=0, description="Number of running jobs")
    completed: int = Field(default=0, description="Number of completed jobs")
    failed: int = Field(default=0, description="Number of failed jobs")
    retryable: int = Field(default=0, description="Number of failed jobs that can be retried")

    @property
    def active_jobs(self) -> int:
        """Number of active (pending + running) jobs."""
        return self.pending + self.running

    @property
    def queue_depth(self) -> int:
        """Current queue depth (pending jobs)."""
        return self.pending
