"""
Temporal Backfill POC — Demonstrates how Temporal prevents the 2026-03-04 incident.

This proof-of-concept shows how the same backfill workload, if orchestrated by
Temporal instead of raw pueue + autonomous agents, would have built-in protection
against every failure mode we experienced:

1. IDEMPOTENCY: Workflow ID = "backfill-BTCUSDT-1000-2024-01" — re-running the
   same workflow is a no-op, not a duplicate. This prevents AP-1 (pueue restart
   creating new tasks).

2. RETRY POLICY: max_attempts=3, non_retryable_errors=["PersistentFailure"] —
   permanent failures (exit code 1, "No data available") stop immediately.
   This prevents AP-2 (restarting persistent failures).

3. RATE LIMITING: max_concurrent_activities=4 — Temporal controls parallelism
   at the worker level. No external agent needed to tune it.

4. NO AUTONOMOUS AGENTS NEEDED: Temporal IS the orchestrator. It handles retries,
   dedup, parallelism, and visibility. The stall detector, autoscaler, and
   overseers become unnecessary.

Usage:
    # Terminal 1: Start Temporal dev server
    temporal server start-dev --db-filename /tmp/temporal-demo.db

    # Terminal 2: Run this script
    uv run --python 3.13 python scripts/temporal_backfill_poc.py

    # Terminal 3: Open Web UI
    open http://localhost:8233
"""

from __future__ import annotations

import asyncio
import hashlib
from dataclasses import dataclass
from datetime import timedelta

import temporalio.client
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.common import RetryPolicy
from temporalio.exceptions import ActivityError
from temporalio.worker import Worker

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class BackfillJob:
    """Input for a single backfill activity."""
    symbol: str
    threshold: int
    year: int
    month: int


@dataclass
class BackfillResult:
    """Output from a backfill activity."""
    symbol: str
    threshold: int
    period: str
    bars_created: int
    status: str


# ---------------------------------------------------------------------------
# Activities (the actual work — equivalent to pueue tasks)
# ---------------------------------------------------------------------------

class PersistentFailure(Exception):
    """Failure that will never succeed on retry (e.g., missing data)."""


def _deterministic_outcome(symbol: str, threshold: int, year: int, month: int) -> tuple[str, int]:
    """Produce a deterministic simulated outcome based on job parameters.

    Uses a hash of the job identity to assign a fixed outcome category
    and bar count, ensuring reproducible POC runs without random state.
    """
    key = f"{symbol}:{threshold}:{year}:{month:02d}"
    digest = hashlib.sha256(key.encode()).hexdigest()
    bucket = int(digest[:2], 16)  # 0-255  # noqa: fake-data

    if bucket < 13:        # ~5% — simulates OOM (transient)
        return "oom", 0
    if bucket < 26:      # ~5% — simulates missing data (persistent)
        return "missing_data", 0
    # ~90% — success
    bars = 100 + (int(digest[2:6], 16) % 4900)  # 100-4999 deterministic  # noqa: fake-data
    return "success", bars


@activity.defn
async def populate_month(job: BackfillJob) -> BackfillResult:
    """
    Simulate populating one month of open deviation bars.

    In production, this would call populate_cache_resumable() via SSH.
    Here we simulate the three outcomes from the incident:
    - Success (most jobs)
    - OOM kill (transient, retryable)
    - Missing data (persistent, NOT retryable)
    """
    period = f"{job.year}-{job.month:02d}"
    activity.logger.info(f"Processing {job.symbol}@{job.threshold} {period}")

    # Simulate processing time (deterministic per-job)
    key = f"{job.symbol}:{job.threshold}:{job.year}:{job.month:02d}"
    delay = 0.5 + (int(hashlib.sha256(key.encode()).hexdigest()[6:8], 16) / 256)  # noqa: fake-data
    await asyncio.sleep(delay)

    outcome, bars = _deterministic_outcome(job.symbol, job.threshold, job.year, job.month)

    if outcome == "oom":
        # Transient — Temporal will retry automatically (up to max_attempts)
        raise RuntimeError(f"OOM killed processing {job.symbol}@{job.threshold} {period}")

    if outcome == "missing_data":
        # PERSISTENT — must NOT retry (non_retryable_error_types catches this)
        raise PersistentFailure(
            f"No data available for {job.symbol}@{job.threshold} {period}"
        )

    # Success
    activity.logger.info(f"Completed {job.symbol}@{job.threshold} {period}: {bars} bars")
    return BackfillResult(
        symbol=job.symbol,
        threshold=job.threshold,
        period=period,
        bars_created=bars,
        status="success",
    )


# ---------------------------------------------------------------------------
# Workflow (the orchestrator — replaces pueue + all 5 autonomous agents)
# ---------------------------------------------------------------------------

@workflow.defn
class BackfillWorkflow:
    """
    Orchestrates backfill for one symbol@threshold across all months.

    This single workflow replaces:
    - pueue job queue (task scheduling)
    - Autoscaler agent (Temporal worker handles concurrency)
    - Stall detector (Temporal retries handle transient failures)
    - Growth monitor (Temporal Web UI provides visibility)
    - Both overseers (Temporal is the overseer)

    KEY GUARDRAIL: The workflow ID is deterministic:
        "backfill-{symbol}-{threshold}-{year}"

    If you try to start the same workflow twice, Temporal rejects it.
    This is the #1 thing that would have prevented the incident.
    """

    @workflow.run
    async def run(self, symbol: str, threshold: int, year: int) -> list[BackfillResult]:
        results: list[BackfillResult] = []
        months = range(1, 13) if year < 2026 else range(1, 4)  # Up to March 2026

        for month in months:
            job = BackfillJob(
                symbol=symbol,
                threshold=threshold,
                year=year,
                month=month,
            )

            try:
                result = await workflow.execute_activity(
                    populate_month,
                    job,
                    start_to_close_timeout=timedelta(minutes=30),
                    retry_policy=RetryPolicy(
                        # KEY: Maximum 3 retries (not infinite like our agents did)
                        maximum_attempts=3,
                        # KEY: Persistent failures are NOT retried
                        # This prevents AP-2 (restarting persistent failures)
                        non_retryable_error_types=["PersistentFailure"],
                        # Exponential backoff (1s, 2s, 4s)
                        initial_interval=timedelta(seconds=1),
                        backoff_coefficient=2.0,
                        maximum_interval=timedelta(seconds=30),
                    ),
                )
                results.append(result)

            except ActivityError as e:
                # Persistent failure — record it and move on to next month
                workflow.logger.warning(
                    f"Permanent failure for {symbol}@{threshold} {year}-{month:02d}: {e}"
                )
                results.append(BackfillResult(
                    symbol=symbol,
                    threshold=threshold,
                    period=f"{year}-{month:02d}",
                    bars_created=0,
                    status=f"failed: {e}",
                ))

        return results


# ---------------------------------------------------------------------------
# Main: Start workflows with idempotent IDs
# ---------------------------------------------------------------------------

async def main():
    # Connect to Temporal dev server
    client = await Client.connect("localhost:7233")

    # Demo: Backfill BTCUSDT@1000 for 2024
    # The workflow ID is deterministic — re-running this script is safe!
    symbols = ["BTCUSDT", "ETHUSDT", "BNBUSDT"]
    threshold = 1000
    year = 2024

    print("=" * 60)
    print("Temporal Backfill POC — Preventing the 2026-03-04 Incident")
    print("=" * 60)
    print()
    print("Starting worker and launching workflows...")
    print("Web UI: http://localhost:8233")
    print()

    # Start a worker that processes our activities
    # max_concurrent_activities=4 replaces the autoscaler agent!
    async with Worker(
        client,
        task_queue="backfill-queue",
        workflows=[BackfillWorkflow],
        activities=[populate_month],
        max_concurrent_activities=4,  # Built-in parallelism control
    ):
        # Launch workflows for each symbol
        handles = []
        for symbol in symbols:
            # KEY: Deterministic workflow ID prevents duplicates!
            workflow_id = f"backfill-{symbol}-{threshold}-{year}"

            print(f"  Starting: {workflow_id}")
            try:
                handle = await client.start_workflow(
                    BackfillWorkflow.run,
                    args=[symbol, threshold, year],
                    id=workflow_id,
                    task_queue="backfill-queue",
                )
            except temporalio.client.WorkflowAlreadyStartedError:
                # THIS is the key guardrail — duplicate rejected, not spawned
                print("    ^ Already exists! Attaching to existing run.")
                handle = client.get_workflow_handle(workflow_id)
            handles.append((symbol, handle))

        # Wait for all workflows to complete
        print()
        print("Workflows running... (check http://localhost:8233 for live status)")
        print()

        all_results = []
        for symbol, handle in handles:
            results = await handle.result()
            all_results.extend(results)

            successes = sum(1 for r in results if r.status == "success")
            failures = sum(1 for r in results if r.status != "success")
            total_bars = sum(r.bars_created for r in results)
            print(f"  {symbol}@{threshold}/{year}: {successes} succeeded, "
                  f"{failures} failed, {total_bars:,} bars")

        print()
        print("=" * 60)
        print("DEMO: Try running this script again — Temporal will reject")
        print("duplicate workflow IDs instead of creating ~12,800 copies!")
        print("=" * 60)

        # Summary
        total_success = sum(1 for r in all_results if r.status == "success")
        total_failed = sum(1 for r in all_results if r.status != "success")
        total_bars = sum(r.bars_created for r in all_results)
        print(f"\nTotal: {total_success} succeeded, {total_failed} permanently failed, "
              f"{total_bars:,} bars created")
        print(f"Total workflows: {len(handles)} (would have been ~12,800 with pueue + agents)")


if __name__ == "__main__":
    asyncio.run(main())
