"""
Scraper service for BYO Apify web scraping.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
from uuid import UUID

import logging
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from .. import settings
from ..models import ScraperJob, ScraperJobStatus
from .apify_adapter import ApifyScraperService
from .scraper_blob_storage import ScraperBlobStorage
from .secret_storage import get_saas_secret_backend

logger = logging.getLogger(__name__)


@dataclass
class ValidationResult:
    rows_with_valid_inputs: int
    rows_skipped: int
    estimated_credits: int
    byo_provider_connected: bool
    errors: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "rows_with_valid_inputs": self.rows_with_valid_inputs,
            "rows_skipped": self.rows_skipped,
            "estimated_credits": self.estimated_credits,
            "byo_provider_connected": self.byo_provider_connected,
            "errors": self.errors,
        }


@dataclass
class TestResult:
    results: List[Dict[str, Any]]
    rows_scraped: int
    rows_skipped: int
    rows_errored: int

    def to_dict(self) -> Dict[str, Any]:
        return {
            "results": self.results,
            "rows_scraped": self.rows_scraped,
            "rows_skipped": self.rows_skipped,
            "rows_errored": self.rows_errored,
        }


@dataclass
class JobCreationResult:
    job_id: UUID
    status: str
    estimated_credits: int
    existing: bool = False

    def to_dict(self) -> Dict[str, Any]:
        return {
            "job_id": str(self.job_id),
            "status": self.status,
            "estimated_credits": self.estimated_credits,
            "existing": self.existing,
        }


@dataclass
class JobStatus:
    job_id: UUID
    status: str
    progress: int
    rows_processed: int
    rows_total: int
    rows_scraped: int
    rows_skipped: int
    rows_errored: int
    error_message: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        data = {
            "job_id": str(self.job_id),
            "status": self.status,
            "progress": self.progress,
            "rows_processed": self.rows_processed,
            "rows_total": self.rows_total,
            "rows_scraped": self.rows_scraped,
            "rows_skipped": self.rows_skipped,
            "rows_errored": self.rows_errored,
        }
        if self.error_message:
            data["error_message"] = self.error_message
        return data


class ScraperService:
    def __init__(self, db: AsyncSession, workspace_id: str):
        self.db = db
        self.workspace_id = workspace_id
        self.blob_storage = ScraperBlobStorage()

    async def list_actors(self) -> List[Dict[str, Any]]:
        return ApifyScraperService.list_actors()

    async def _get_apify_token(self) -> Optional[str]:
        backend = get_saas_secret_backend()
        secret = await backend.get_secret(
            db=self.db,
            integration="apify",
            key="api_token",
            workspace_id=self.workspace_id,
        )
        return secret.value if secret else None

    async def validate_inputs(
        self,
        rows: List[Dict[str, Any]],
        input_mapping: Dict[str, str],
        actor_type: str,
    ) -> ValidationResult:
        token = await self._get_apify_token()
        service = ApifyScraperService(api_token=token or "") if token else None
        connected = bool(token)
        if not service:
            return ValidationResult(
                rows_with_valid_inputs=0,
                rows_skipped=len(rows),
                estimated_credits=0,
                byo_provider_connected=False,
                errors=["Apify not connected"],
            )
        counts = service.validate_rows(rows, input_mapping, actor_type)
        return ValidationResult(
            rows_with_valid_inputs=counts["rows_with_valid_inputs"],
            rows_skipped=counts["rows_skipped"],
            estimated_credits=counts["estimated_credits"],
            byo_provider_connected=connected,
        )

    async def run_test(
        self,
        rows: List[Dict[str, Any]],
        input_mapping: Dict[str, str],
        output_fields: List[str],
        actor_type: str,
        max_rows: int = 5,
    ) -> TestResult:
        token = await self._get_apify_token()
        if not token:
            raise ValueError("Apify not connected")
        service = ApifyScraperService(api_token=token)
        actor = ApifyScraperService.get_actor(actor_type)
        output_fields = self._resolve_output_fields(actor, output_fields)
        results = service.run_test(
            rows=rows,
            input_mapping=input_mapping,
            output_fields=output_fields,
            actor_type=actor_type,
            max_rows=max_rows,
        )
        rows_scraped = sum(1 for r in results if r.get("status") == "scraped")
        rows_skipped = sum(1 for r in results if r.get("status") == "skipped")
        rows_errored = sum(1 for r in results if r.get("status") == "error")
        return TestResult(
            results=results,
            rows_scraped=rows_scraped,
            rows_skipped=rows_skipped,
            rows_errored=rows_errored,
        )

    @staticmethod
    def _resolve_output_fields(actor: Any, output_fields: List[str]) -> List[str]:
        if output_fields:
            valid = {field.key for field in actor.output_schema}
            invalid = [field for field in output_fields if field not in valid]
            if invalid:
                raise ValueError(f"Invalid output fields: {', '.join(invalid)}")
            return output_fields
        return [field.key for field in actor.output_schema if field.default]

    async def create_job(
        self,
        rows: List[Dict[str, Any]],
        actor_type: str,
        input_mapping: Dict[str, str],
        output_fields: List[str],
        run_id: str,
        created_by: Optional[str] = None,
        notify_email: Optional[str] = None,
        sheet_id: Optional[str] = None,
        sheet_name: Optional[str] = None,
    ) -> JobCreationResult:
        if len(rows) > settings.SCRAPER_MAX_ROWS:
            raise ValueError(
                f"Row limit exceeded: max {settings.SCRAPER_MAX_ROWS} rows per request, "
                f"received {len(rows)}"
            )

        existing = await self.db.execute(
            select(ScraperJob).where(
                ScraperJob.workspace_id == self.workspace_id,
                ScraperJob.run_id == run_id,
            )
        )
        existing_job = existing.scalar_one_or_none()
        if existing_job:
            return JobCreationResult(
                job_id=existing_job.id,
                status=existing_job.status,
                estimated_credits=existing_job.rows_total,
                existing=True,
            )

        token = await self._get_apify_token()
        if not token:
            raise ValueError("Apify not connected")
        actor = ApifyScraperService.get_actor(actor_type)
        output_fields = self._resolve_output_fields(actor, output_fields)
        expires_at = datetime.now(timezone.utc) + timedelta(seconds=settings.SCRAPER_DATA_TTL_SECONDS)

        job = ScraperJob(
            workspace_id=self.workspace_id,
            run_id=run_id,
            status=ScraperJobStatus.QUEUED.value,
            actor_type=actor.id,
            actor_id=actor.actor_id,
            input_mapping=input_mapping,
            output_fields=output_fields,
            rows_total=len(rows),
            rows_processed=0,
            rows_scraped=0,
            rows_skipped=0,
            rows_errored=0,
            cursor=0,
            notify_email=notify_email,
            sheet_id=sheet_id,
            sheet_name=sheet_name,
            expires_at=expires_at,
        )
        self.db.add(job)
        await self.db.commit()
        await self.db.refresh(job)

        inputs_uri = await self.blob_storage.store_inputs(self.workspace_id, job.id, rows)
        results_uri = await self.blob_storage.store_results(self.workspace_id, job.id, [])
        job.inputs_uri = inputs_uri
        job.results_uri = results_uri
        await self.db.commit()

        await self._enqueue_background_job(job.id)

        return JobCreationResult(
            job_id=job.id,
            status=job.status,
            estimated_credits=len(rows),
            existing=False,
        )

    async def _enqueue_background_job(self, job_id: UUID) -> None:
        from anyio.to_thread import run_sync
        from rq import Queue
        from ..worker import conn as _rq_conn

        def _do_enqueue() -> None:
            Queue(settings.RQ_QUEUE_NAME, connection=_rq_conn).enqueue(
                "fmatch.saas.worker.run_scraper_job",
                str(job_id),
                job_timeout=settings.JOB_TIMEOUT_DEFAULT,
            )

        await run_sync(_do_enqueue)

    async def get_job_status(self, job_id: UUID) -> Optional[JobStatus]:
        result = await self.db.execute(
            select(ScraperJob).where(
                ScraperJob.id == job_id,
                ScraperJob.workspace_id == self.workspace_id,
            )
        )
        job = result.scalar_one_or_none()
        if not job:
            return None

        progress = 0
        if job.rows_total > 0:
            progress = int((job.rows_processed / job.rows_total) * 100)

        return JobStatus(
            job_id=job.id,
            status=job.status,
            progress=progress,
            rows_processed=job.rows_processed,
            rows_total=job.rows_total,
            rows_scraped=job.rows_scraped,
            rows_skipped=job.rows_skipped,
            rows_errored=job.rows_errored,
            error_message=job.error_message,
        )

    async def get_results_chunk(self, job_id: UUID, cursor: int, limit: int) -> Dict[str, Any]:
        return await self.blob_storage.get_results_chunk(self.workspace_id, job_id, cursor, limit)

    async def cancel_job(self, job_id: UUID) -> bool:
        job = await self.db.get(ScraperJob, job_id)
        if not job or str(job.workspace_id) != str(self.workspace_id):
            return False
        if job.status in (ScraperJobStatus.COMPLETED.value, ScraperJobStatus.FAILED.value):
            return False
        job.status = ScraperJobStatus.CANCELLED.value
        job.completed_at = datetime.now(timezone.utc)
        await self.db.commit()
        try:
            await self.blob_storage.delete_blobs(self.workspace_id, job_id)
        except Exception as exc:
            logger.warning("[SCRAPER] Failed to delete blobs for %s: %s", job_id, exc)
        return True
