from datetime import datetime, timezone
from typing import Any

from celery.result import AsyncResult
from sqlalchemy import asc, desc, func, or_
from sqlalchemy.orm import Session


from .models import TaskRun, TaskStatus

# No usar valid_rows/total_rows del resultado final: en SUCCESS distorsiona el % (errores de scan).
_CELERY_TERMINAL_RAW = frozenset({"SUCCESS", "FAILURE", "REVOKED", "REJECTED"})


def _progress_from_row_count_dict(d: dict[str, Any]) -> tuple[int, int] | None:
    total = d.get("total_rows")
    if not isinstance(total, (int, float)) or total <= 0:
        return None
    valid = d.get("valid_rows")
    if isinstance(valid, (int, float)):
        return int(valid), int(total)
    processed = d.get("processed_rows")
    if isinstance(processed, (int, float)):
        return int(processed), int(total)
    return None


def normalize_celery_state(state: str | None) -> TaskStatus:
    raw = (state or "PENDING").upper()
    if raw in {"STARTED", "PROGRESS", "PROCESSING", "RETRY"}:
        return TaskStatus.ACTIVE
    if raw == "SUCCESS":
        return TaskStatus.COMPLETED
    if raw in {"FAILURE", "REVOKED", "REJECTED"}:
        return TaskStatus.FAILED
    return TaskStatus.PENDING


def _extract_progress(result: AsyncResult, raw_state_upper: str) -> tuple[int | None, int | None, float | None, str | None]:
    payload = result.info if isinstance(result.info, dict) else {}
    current = payload.get("current")
    total = payload.get("total")
    message = payload.get("status")

    if current is None and isinstance(result.result, dict):
        current = result.result.get("current")
    if total is None and isinstance(result.result, dict):
        total = result.result.get("total")
    if message is None and isinstance(result.result, dict):
        message = result.result.get("status")

    percent = None
    if isinstance(current, (int, float)) and isinstance(total, (int, float)) and total > 0:
        percent = min(100.0, max(0.0, (float(current) / float(total)) * 100.0))

    raw = (raw_state_upper or "PENDING").upper()
    if percent is None and raw not in _CELERY_TERMINAL_RAW:
        for src in (payload, result.result if isinstance(result.result, dict) else {}):
            if not isinstance(src, dict):
                continue
            pair = _progress_from_row_count_dict(src)
            if pair is None:
                continue
            cur_i, tot_i = pair
            current, total = cur_i, tot_i
            percent = min(100.0, max(0.0, (float(cur_i) / float(tot_i)) * 100.0))
            break

    return (
        int(current) if isinstance(current, (int, float)) else None,
        int(total) if isinstance(total, (int, float)) else None,
        percent,
        str(message) if message is not None else None,
    )


def _extract_failure(result: AsyncResult) -> tuple[str | None, str | None, str | None]:
    exception_type = None
    exception_message = None
    traceback_excerpt = None

    err = result.result
    if isinstance(err, Exception):
        exception_type = type(err).__name__
        exception_message = str(err)
    elif isinstance(err, dict):
        exception_type = err.get("exc_type")
        exception_message = err.get("exc_message") or err.get("error") or err.get("message")
    elif err is not None:
        exception_message = str(err)

    tb = getattr(result, "traceback", None)
    if isinstance(tb, str):
        traceback_excerpt = tb[-4000:]

    return exception_type, exception_message, traceback_excerpt


class TaskTrackerService:
    def __init__(self, db: Session):
        self.db = db

    def register_dispatch(
        self,
        *,
        task_id: str,
        tenant_id: int,
        task_name: str,
        task_group: str,
        company_id: int | None = None,
        requested_by_user: str | None = None,
        task_origin: str | None = None,
        meta_payload: dict[str, Any] | None = None,
    ) -> TaskRun:
        current = self.db.query(TaskRun).filter(TaskRun.task_id == task_id).first()
        if current:
            return current

        row = TaskRun(
            task_id=task_id,
            tenant_id=tenant_id,
            company_id=company_id,
            requested_by_user=requested_by_user,
            task_name=task_name,
            task_group=task_group,
            task_origin=task_origin,
            status=TaskStatus.PENDING.value,
            celery_state_raw="PENDING",
            progress_current=0,
            progress_total=100,
            progress_percent=0.0,
            progress_message="Queued",
            retries=0,
            meta_payload=meta_payload,
            started_at=None,
            finished_at=None,
        )
        self.db.add(row)
        self.db.commit()
        self.db.refresh(row)
        return row

    def sync_task(self, task_run: TaskRun) -> TaskRun:
        from core.celery_app import celery_app
        async_result = celery_app.AsyncResult(task_run.task_id)
        raw_state = (async_result.state or "PENDING").upper()
        normalized = normalize_celery_state(raw_state)
        now = datetime.now(timezone.utc)

        task_run.celery_state_raw = raw_state
        task_run.status = normalized.value
        task_run.retries = int(getattr(async_result, "retries", 0) or 0)

        current, total, percent, message = _extract_progress(async_result, raw_state)
        if current is not None:
            task_run.progress_current = current
        if total is not None:
            task_run.progress_total = total
        if percent is not None:
            task_run.progress_percent = percent
        if message:
            task_run.progress_message = message

        if normalized == TaskStatus.ACTIVE and task_run.started_at is None:
            task_run.started_at = now

        if normalized == TaskStatus.COMPLETED:
            if task_run.started_at is None:
                task_run.started_at = now
            task_run.finished_at = now
            task_run.exception_type = None
            task_run.exception_message = None
            task_run.traceback_excerpt = None
            if isinstance(async_result.result, dict):
                task_run.result_summary = async_result.result
            else:
                task_run.result_summary = {"result": str(async_result.result)}
            # register_dispatch seeds progress_percent=0; Celery SUCCESS often has no current/total meta
            if percent is None:
                task_run.progress_percent = 100.0

        if normalized == TaskStatus.FAILED:
            if task_run.started_at is None:
                task_run.started_at = now
            task_run.finished_at = now
            etype, emsg, tb = _extract_failure(async_result)
            task_run.exception_type = etype
            task_run.exception_message = emsg
            task_run.traceback_excerpt = tb

        self.db.add(task_run)
        self.db.commit()
        self.db.refresh(task_run)
        return task_run

    def sync_active_tasks(self, tenant_id: int, task_ids: list[str] | None = None) -> int:
        query = self.db.query(TaskRun).filter(
            TaskRun.tenant_id == tenant_id, TaskRun.status.in_([TaskStatus.PENDING.value, TaskStatus.ACTIVE.value])
        )
        if task_ids:
            query = query.filter(TaskRun.task_id.in_(task_ids))
        rows = query.limit(200).all()
        for row in rows:
            self.sync_task(row)
        return len(rows)

    def list_tasks(
        self,
        *,
        tenant_id: int,
        page: int,
        page_size: int,
        status: list[str] | None = None,
        task_group: list[str] | None = None,
        task_name: list[str] | None = None,
        company_id: int | None = None,
        search: str | None = None,
        order: str = "desc",
    ) -> tuple[list[TaskRun], int]:
        query = self.db.query(TaskRun).filter(TaskRun.tenant_id == tenant_id)
        if status:
            query = query.filter(TaskRun.status.in_(status))
        if task_group:
            query = query.filter(TaskRun.task_group.in_(task_group))
        if task_name:
            query = query.filter(TaskRun.task_name.in_(task_name))
        if company_id is not None:
            # Tareas sin company_id (p. ej. reportes solo por tenant) deben seguir visibles
            query = query.filter(or_(TaskRun.company_id == company_id, TaskRun.company_id.is_(None)))
        if search:
            term = f"%{search}%"
            query = query.filter(
                (TaskRun.task_id.ilike(term))
                | (TaskRun.task_name.ilike(term))
                | (TaskRun.task_origin.ilike(term))
                | (TaskRun.exception_message.ilike(term))
            )

        total = query.with_entities(func.count(TaskRun.id)).scalar() or 0
        order_expr = asc(TaskRun.updated_at) if order == "asc" else desc(TaskRun.updated_at)
        items = query.order_by(order_expr).offset((page - 1) * page_size).limit(page_size).all()
        return items, total
