"""Database-backed job manager using Supabase + Redis pub/sub for cross-worker job updates."""
import asyncio
import json
from datetime import datetime, timezone, timedelta
from typing import Dict, Any, Optional
from loguru import logger

# BackgroundJob, JobType, JobStatus are defined in background_jobs.py.
# We import them here; background_jobs.py imports job_manager from this module at
# its bottom — Python handles this circular reference safely because background_jobs.py
# defines these symbols *before* it reaches the import-from-db_job_manager line.
from src.services.background_jobs import BackgroundJob, JobType, JobStatus


def _parse_ts(value: Optional[str]) -> Optional[datetime]:
    """Parse an ISO-8601 / Supabase timestamp string to a timezone-aware datetime."""
    if not value:
        return None
    try:
        # Supabase returns strings like "2024-01-01T12:00:00+00:00" or ending in "Z"
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return None


class DBJobManager:
    """Manages background jobs backed by the Supabase `background_jobs` table.

    Publishes job-update payloads to Redis channel ``job_updates:{user_id}`` so
    every worker process can push SSE events to its own connected clients.
    """

    def _db(self):
        """Lazy DatabaseClient — avoids import at module load time."""
        from src.database.client import DatabaseClient
        if not hasattr(self, "_db_client"):
            self._db_client = DatabaseClient()
        return self._db_client

    def _row_to_job(self, row: dict) -> BackgroundJob:
        return BackgroundJob(
            id=row["id"],
            job_type=JobType(row["job_type"]),
            user_id=row["user_id"],
            status=JobStatus(row["status"]),
            progress=row.get("progress") or 0,
            message=row.get("message") or "",
            error=row.get("error"),
            result=row.get("result"),
            created_at=_parse_ts(row.get("created_at")) or datetime.now(timezone.utc),
            started_at=_parse_ts(row.get("started_at")),
            completed_at=_parse_ts(row.get("completed_at")),
        )

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def create_job(self, job_type: JobType, user_id: str) -> BackgroundJob:
        """Create a new background job row in the database."""
        db = self._db()
        try:
            resp = await asyncio.to_thread(
                lambda: db.client.table("background_jobs").insert({
                    "job_type": job_type.value,
                    "user_id": user_id,
                    "status": JobStatus.PENDING.value,
                    "progress": 0,
                    "message": "",
                }).execute()
            )
            job = self._row_to_job(resp.data[0])
            logger.info(f"Created background job {job.id} of type {job_type} for user {user_id}")
            return job
        except Exception as e:
            logger.error(f"Failed to create job in DB: {e}")
            raise

    async def get_job(self, job_id: str) -> Optional[BackgroundJob]:
        """Fetch a single job by ID from the database."""
        db = self._db()
        try:
            resp = await asyncio.to_thread(
                lambda: db.client.table("background_jobs").select("*").eq("id", job_id).execute()
            )
            if resp.data:
                return self._row_to_job(resp.data[0])
            return None
        except Exception as e:
            logger.warning(f"Failed to get job {job_id}: {e}")
            return None

    async def get_user_jobs(self, user_id: str) -> list[BackgroundJob]:
        """Fetch the 50 most-recent jobs for a user from the database."""
        db = self._db()
        try:
            resp = await asyncio.to_thread(
                lambda: db.client.table("background_jobs").select("*")
                    .eq("user_id", user_id)
                    .order("created_at", desc=True)
                    .limit(50)
                    .execute()
            )
            return [self._row_to_job(r) for r in resp.data]
        except Exception as e:
            logger.warning(f"Failed to get jobs for user {user_id}: {e}")
            return []

    async def get_active_jobs(self, user_id: str) -> list[BackgroundJob]:
        """Fetch pending/running jobs for a user from the database."""
        db = self._db()
        try:
            resp = await asyncio.to_thread(
                lambda: db.client.table("background_jobs").select("*")
                    .eq("user_id", user_id)
                    .in_("status", [JobStatus.PENDING.value, JobStatus.RUNNING.value])
                    .order("created_at", desc=True)
                    .execute()
            )
            return [self._row_to_job(r) for r in resp.data]
        except Exception as e:
            logger.warning(f"Failed to get active jobs for user {user_id}: {e}")
            return []

    async def update_job(
        self,
        job_id: str,
        status: Optional[JobStatus] = None,
        progress: Optional[int] = None,
        message: Optional[str] = None,
        error: Optional[str] = None,
        result: Optional[Dict[str, Any]] = None,
    ):
        """Update a job row in the database, then publish the new state to Redis."""
        db = self._db()
        update_data: dict = {}

        if status is not None:
            update_data["status"] = status.value
            if status == JobStatus.RUNNING:
                update_data["started_at"] = datetime.now(timezone.utc).isoformat()
            elif status in (JobStatus.COMPLETED, JobStatus.FAILED):
                update_data["completed_at"] = datetime.now(timezone.utc).isoformat()
        if progress is not None:
            update_data["progress"] = progress
        if message is not None:
            update_data["message"] = message
        if error is not None:
            update_data["error"] = error
        if result is not None:
            update_data["result"] = result

        if not update_data:
            return

        try:
            resp = await asyncio.to_thread(
                lambda: db.client.table("background_jobs")
                    .update(update_data)
                    .eq("id", job_id)
                    .execute()
            )
            if resp.data:
                job = self._row_to_job(resp.data[0])
                await self._publish_update(job)
        except Exception as e:
            logger.error(f"Failed to update job {job_id} in DB: {e}")

    async def cleanup_old_jobs(self, max_age_hours: int = 24):
        """Delete completed/failed jobs older than *max_age_hours* from the database."""
        db = self._db()
        try:
            cutoff = (datetime.now(timezone.utc) - timedelta(hours=max_age_hours)).isoformat()
            await asyncio.to_thread(
                lambda: db.client.table("background_jobs")
                    .delete()
                    .in_("status", [JobStatus.COMPLETED.value, JobStatus.FAILED.value])
                    .lt("completed_at", cutoff)
                    .execute()
            )
            logger.info(f"Cleaned up background jobs older than {max_age_hours}h")
        except Exception as e:
            logger.warning(f"Failed to cleanup old jobs: {e}")

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    async def _publish_update(self, job: BackgroundJob):
        """Publish a job-update payload to Redis pub/sub so all workers can forward it."""
        try:
            from src.services.redis_client import get_redis
            r = await get_redis()
            payload = json.dumps({
                "id": job.id,
                "job_type": job.job_type.value,
                "status": job.status.value,
                "progress": job.progress,
                "message": job.message,
                "error": job.error,
            })
            await r.publish(f"job_updates:{job.user_id}", payload)
        except Exception as e:
            logger.warning(f"Failed to publish job update to Redis: {e}")


# Global singleton — replaces the old BackgroundJobManager instance
job_manager = DBJobManager()
