"""Single-use task-token ledger interfaces and an in-memory test implementation."""

from __future__ import annotations

import threading
from dataclasses import dataclass
from typing import Mapping, Optional

from sqlalchemy import text


@dataclass
class TaskLedgerRecord:
    jti: str
    binding: Mapping[str, object]
    expires_at: int
    status: str = "running"
    commit_outcome: str = "not_applicable"
    safe_detail: str = ""


class InMemoryTaskLedger:
    def __init__(self):
        self._records = {}
        self._lock = threading.Lock()

    def claim(self, jti, binding, *, expires_at):
        with self._lock:
            if jti in self._records:
                return False
            self._records[jti] = TaskLedgerRecord(
                jti=str(jti),
                binding=dict(binding),
                expires_at=int(expires_at),
            )
            return True

    def finish(
        self,
        jti,
        *,
        status,
        commit_outcome="not_applicable",
        safe_detail="",
    ):
        with self._lock:
            record = self._records[str(jti)]
            record.status = str(status)
            record.commit_outcome = str(commit_outcome)
            record.safe_detail = str(safe_detail)[:500]

    def get(self, jti) -> Optional[TaskLedgerRecord]:
        with self._lock:
            return self._records.get(str(jti))


class PostgresTaskLedger:
    """Durable, cross-worker single-use ledger backed by the platform DB."""

    def __init__(self, engine):
        self.engine = engine

    def claim(self, jti, binding, *, expires_at):
        parameters = {
            "jti": str(jti),
            "task_uid": binding["task_uid"],
            "dataflow_uid": binding["dataflow_uid"],
            "workflow_version": int(binding["workflow_version"]),
            "correlation_id": binding["correlation_id"],
            "node_id": binding["node_id"],
            "node_type": binding["node_type"],
            "data_source_uid": binding.get("data_source_uid"),
            "idempotency_key": binding.get("idempotency_key"),
            "expires_at": int(expires_at),
        }
        with self.engine.begin() as connection:
            result = connection.execute(
                text(
                    """
                    INSERT INTO public.runner_task_executions (
                        token_jti, task_uid, dataflow_uid, workflow_version,
                        correlation_id, node_id, node_type, data_source_uid,
                        idempotency_key, status, commit_outcome, expires_at
                    ) VALUES (
                        CAST(:jti AS uuid), CAST(:task_uid AS uuid),
                        CAST(:dataflow_uid AS uuid), :workflow_version,
                        CAST(:correlation_id AS uuid), :node_id, :node_type,
                        CAST(:data_source_uid AS uuid), :idempotency_key,
                        'running', 'not_applicable',
                        to_timestamp(:expires_at)
                    )
                    ON CONFLICT DO NOTHING
                    """
                ),
                parameters,
            )
        return int(result.rowcount or 0) == 1

    def finish(
        self,
        jti,
        *,
        status,
        commit_outcome="not_applicable",
        safe_detail="",
    ):
        allowed_statuses = {"success", "failed", "unknown"}
        allowed_outcomes = {
            "not_applicable",
            "not_committed",
            "committed",
            "unknown",
        }
        if status not in allowed_statuses or commit_outcome not in allowed_outcomes:
            raise ValueError("runner task outcome is invalid")
        with self.engine.begin() as connection:
            connection.execute(
                text(
                    """
                    UPDATE public.runner_task_executions
                    SET status = :status,
                        commit_outcome = :commit_outcome,
                        safe_detail = :safe_detail,
                        finished_at = CURRENT_TIMESTAMP
                    WHERE token_jti = CAST(:jti AS uuid)
                      AND status = 'running'
                    """
                ),
                {
                    "jti": str(jti),
                    "status": status,
                    "commit_outcome": commit_outcome,
                    "safe_detail": str(safe_detail)[:500],
                },
            )

    def get(self, jti):
        with self.engine.begin() as connection:
            row = (
                connection.execute(
                    text(
                        """
                        SELECT token_jti::text AS jti, task_uid::text,
                               node_id, data_source_uid::text,
                               idempotency_key, status, commit_outcome,
                               safe_detail, EXTRACT(EPOCH FROM expires_at)::bigint
                                   AS expires_at
                        FROM public.runner_task_executions
                        WHERE token_jti = CAST(:jti AS uuid)
                        """
                    ),
                    {"jti": str(jti)},
                )
                .mappings()
                .one_or_none()
            )
        if row is None:
            return None
        return TaskLedgerRecord(
            jti=row["jti"],
            binding={
                "task_uid": row["task_uid"],
                "node_id": row["node_id"],
                "data_source_uid": row["data_source_uid"],
                "idempotency_key": row["idempotency_key"],
            },
            expires_at=int(row["expires_at"]),
            status=row["status"],
            commit_outcome=row["commit_outcome"],
            safe_detail=row["safe_detail"] or "",
        )
