from __future__ import annotations

import datetime as dt
import uuid
from typing import Any, Dict, Optional

import structlog

from app.models.scan import ScanJob, ScanJobStatus, ScanRequest, ScanResult
from app.services.firestore import get_firestore_client
from app.services.scanner import ScannerError, execute_scan

_LOGGER = structlog.get_logger(__name__)

SCAN_JOBS_COLLECTION = "scanJobs"


def _collection():
    db = get_firestore_client()
    return db.collection(SCAN_JOBS_COLLECTION)


def _serialize_result(result: ScanResult) -> Dict[str, Any]:
    return result.model_dump(by_alias=True)


def enqueue_scan(request: ScanRequest, user_id: str) -> ScanJob:
    queued_at = dt.datetime.now(dt.timezone.utc)
    job_id = uuid.uuid4().hex[:12]
    payload = {
        "jobId": job_id,
        "userId": user_id,
        "status": ScanJobStatus.QUEUED.value,
        "prompt": request.prompt,
        "categories": request.categories,
        "queuedAt": queued_at,
        "request": request.model_dump(by_alias=True),
    }

    _collection().document(job_id).set(payload)

    return ScanJob(
        job_id=job_id,
        status=ScanJobStatus.QUEUED,
        prompt=request.prompt,
        categories=request.categories,
        queued_at=queued_at,
    )


def get_job(job_id: str, user_id: str) -> ScanJob:
    snapshot = _collection().document(job_id).get()
    if not snapshot.exists:
        raise ScannerError("Scan job not found")

    data = snapshot.to_dict()
    if data.get("userId") != user_id:
        raise ScannerError("Not authorized to access this scan job")

    return _hydrate_job(data)


def process_scan_job(job_id: str) -> Optional[ScanResult]:
    doc_ref = _collection().document(job_id)
    snapshot = doc_ref.get()
    if not snapshot.exists:
        _LOGGER.warning("scan_job.missing", job_id=job_id)
        return None

    data = snapshot.to_dict()
    if data.get("status") not in {ScanJobStatus.QUEUED.value, ScanJobStatus.PROCESSING.value}:
        # Job already processed
        return data.get("result")

    user_id = data.get("userId")
    request_data = data.get("request") or {}
    try:
        request = ScanRequest.model_validate(request_data)
    except Exception as exc:  # pragma: no cover - defensive
        doc_ref.update(
            {
                "status": ScanJobStatus.FAILED.value,
                "errorMessage": "Invalid job payload",
                "completedAt": dt.datetime.now(dt.timezone.utc),
            }
        )
        _LOGGER.warning("scan_job.invalid_payload", job_id=job_id, error=str(exc))
        return None

    started_at = dt.datetime.now(dt.timezone.utc)
    doc_ref.update({
        "status": ScanJobStatus.PROCESSING.value,
        "startedAt": started_at,
    })

    try:
        result = execute_scan(request, user_id=user_id)
        completed_at = dt.datetime.now(dt.timezone.utc)
        doc_ref.update(
            {
                "status": ScanJobStatus.COMPLETED.value,
                "completedAt": completed_at,
                "result": _serialize_result(result),
            }
        )
        return result
    except ScannerError as exc:
        completed_at = dt.datetime.now(dt.timezone.utc)
        doc_ref.update(
            {
                "status": ScanJobStatus.FAILED.value,
                "completedAt": completed_at,
                "errorMessage": str(exc),
            }
        )
        _LOGGER.info("scan_job.failed", job_id=job_id, error=str(exc))
        return None
    except Exception as exc:  # pragma: no cover - handled via tests for explicit ScannerError
        completed_at = dt.datetime.now(dt.timezone.utc)
        doc_ref.update(
            {
                "status": ScanJobStatus.FAILED.value,
                "completedAt": completed_at,
                "errorMessage": str(exc),
            }
        )
        _LOGGER.exception("scan_job.failed", job_id=job_id, error=str(exc))
        return None


def _hydrate_job(data: Dict[str, Any]) -> ScanJob:
    queued_at = data.get("queuedAt")
    started_at = data.get("startedAt")
    completed_at = data.get("completedAt")
    result_data = data.get("result")

    if result_data:
        try:
            result = ScanResult.model_validate(result_data)
        except Exception:
            result = None
    else:
        result = None

    return ScanJob(
        job_id=data.get("jobId"),
        status=ScanJobStatus(data.get("status")),
        prompt=data.get("prompt", ""),
        categories=data.get("categories") or [],
        queued_at=_to_datetime(queued_at),
        started_at=_to_datetime(started_at),
        completed_at=_to_datetime(completed_at),
        result=result,
        error_message=data.get("errorMessage"),
    )


def _to_datetime(value: Any) -> Optional[dt.datetime]:
    if value is None:
        return None
    if isinstance(value, dt.datetime):
        if value.tzinfo is None:
            return value.replace(tzinfo=dt.timezone.utc)
        return value
    if hasattr(value, "to_datetime"):
        # Firestore Timestamp protos support to_datetime()
        return value.to_datetime()
    return None
