"""
Output Queue Manager - Handles failed task output storage and retry.

When a 'processed' call fails (host unreachable, 500 error, etc.), the output
is stored locally and retried later with exponential backoff.
"""

from __future__ import annotations

import json
import shutil
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from iq_sdk.config import RetryConfig


@dataclass
class QueuedOutput:
    """Represents a queued output waiting for retry."""

    task_id: str
    tool_name: str
    site_id: str
    output: dict[str, Any]
    status: str
    retry_count: int
    retry_at: datetime
    created_at: datetime
    file_path: Path

    @classmethod
    def from_file(cls, file_path: Path) -> QueuedOutput | None:
        """Load a queued output from a JSON file."""
        try:
            with open(file_path) as f:
                data = json.load(f)

            return cls(
                task_id=data["task_id"],
                tool_name=data["tool_name"],
                site_id=data["site_id"],
                output=data["output"],
                status=data["status"],
                retry_count=data["retry_count"],
                retry_at=datetime.fromisoformat(data["retry_at"]),
                created_at=datetime.fromisoformat(data["created_at"]),
                file_path=file_path,
            )
        except (json.JSONDecodeError, KeyError, ValueError):
            return None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "task_id": self.task_id,
            "tool_name": self.tool_name,
            "site_id": self.site_id,
            "output": self.output,
            "status": self.status,
            "retry_count": self.retry_count,
            "retry_at": self.retry_at.isoformat(),
            "created_at": self.created_at.isoformat(),
        }

    def is_due(self) -> bool:
        """Check if this output is due for retry."""
        return datetime.now() >= self.retry_at


class OutputQueueManager:
    """
    Manages queued outputs for retry.

    Directory structure:
        ./data/{site}/queue-output/           - Pending outputs
        ./data/{site}/queue-output/failed/    - Failed outputs (max retries exceeded)
    """

    def __init__(
        self,
        base_path: str | Path,
        site_id: str,
        retry_config: RetryConfig | None = None,
    ):
        """
        Initialize the output queue manager.

        Args:
            base_path: Base path for data directory (usually project root)
            site_id: Site identifier
            retry_config: Retry configuration (uses defaults if None)
        """
        self.base_path = Path(base_path)
        self.site_id = site_id

        # Default retry config if not provided
        if retry_config is None:
            from iq_sdk.config import RetryConfig

            retry_config = RetryConfig()

        self.retry_config = retry_config

        # Setup directories
        self.queue_dir = self.base_path / "data" / site_id / "queue-output"
        self.failed_dir = self.queue_dir / "failed"

        self.queue_dir.mkdir(parents=True, exist_ok=True)
        self.failed_dir.mkdir(parents=True, exist_ok=True)

    def queue_output(
        self,
        task_id: str,
        tool_name: str,
        output: dict[str, Any],
        status: str,
        retry_count: int = 0,
    ) -> Path:
        """
        Queue a failed output for later retry.

        Args:
            task_id: Task identifier
            tool_name: Tool name
            output: Output data to send
            status: Task status (Processed/Failed)
            retry_count: Current retry count

        Returns:
            Path to the created queue file
        """
        now = datetime.now()

        # Calculate next retry time with exponential backoff
        delay = min(
            self.retry_config.initial_delay * (self.retry_config.backoff_multiplier**retry_count),
            self.retry_config.max_delay,
        )
        retry_at = now + timedelta(seconds=delay)

        queued = QueuedOutput(
            task_id=task_id,
            tool_name=tool_name,
            site_id=self.site_id,
            output=output,
            status=status,
            retry_count=retry_count,
            retry_at=retry_at,
            created_at=now,
            file_path=Path(),  # Will be set below
        )

        # Create filename: {task_id}__{retry_count}.json
        filename = f"{task_id}__{retry_count}.json"
        file_path = self.queue_dir / filename

        with open(file_path, "w") as f:
            json.dump(queued.to_dict(), f, indent=2)

        return file_path

    def get_pending_outputs(
        self,
        tool_filter: str | None = None,
        task_id_filter: str | None = None,
        due_only: bool = False,
    ) -> list[QueuedOutput]:
        """
        Get pending outputs from the queue.

        Args:
            tool_filter: Filter by tool name (substring match)
            task_id_filter: Filter by task ID (exact or substring match)
            due_only: Only return outputs that are due for retry

        Returns:
            List of queued outputs
        """
        outputs = []

        for file_path in self.queue_dir.glob("*.json"):
            if file_path.name == "failed":
                continue

            queued = QueuedOutput.from_file(file_path)
            if queued is None:
                continue

            # Apply filters
            if tool_filter and tool_filter not in queued.tool_name:
                continue
            if task_id_filter and task_id_filter not in queued.task_id:
                continue
            if due_only and not queued.is_due():
                continue

            outputs.append(queued)

        # Sort by retry_at (oldest first)
        outputs.sort(key=lambda x: x.retry_at)
        return outputs

    def get_failed_outputs(
        self,
        tool_filter: str | None = None,
        task_id_filter: str | None = None,
    ) -> list[QueuedOutput]:
        """
        Get failed outputs (max retries exceeded).

        Args:
            tool_filter: Filter by tool name (substring match)
            task_id_filter: Filter by task ID

        Returns:
            List of failed outputs
        """
        outputs = []

        for file_path in self.failed_dir.glob("*.json"):
            queued = QueuedOutput.from_file(file_path)
            if queued is None:
                continue

            # Apply filters
            if tool_filter and tool_filter not in queued.tool_name:
                continue
            if task_id_filter and task_id_filter not in queued.task_id:
                continue

            outputs.append(queued)

        # Sort by created_at (oldest first)
        outputs.sort(key=lambda x: x.created_at)
        return outputs

    def remove_output(self, queued: QueuedOutput) -> None:
        """Remove a queued output after successful retry."""
        if queued.file_path.exists():
            queued.file_path.unlink()

    def move_to_failed(self, queued: QueuedOutput) -> Path:
        """Move a queued output to the failed directory."""
        if not queued.file_path.exists():
            # File doesn't exist, nothing to move
            return queued.file_path

        # Create new filename in failed directory
        new_path = self.failed_dir / queued.file_path.name
        shutil.move(str(queued.file_path), str(new_path))
        return new_path

    def increment_retry(self, queued: QueuedOutput) -> QueuedOutput | None:
        """
        Increment retry count and reschedule.

        If max retries exceeded, moves to failed directory.

        Args:
            queued: The queued output to update

        Returns:
            Updated QueuedOutput, or None if moved to failed
        """
        new_retry_count = queued.retry_count + 1

        if new_retry_count >= self.retry_config.max_retries:
            # Max retries exceeded, move to failed
            self.move_to_failed(queued)
            return None

        # Remove old file
        if queued.file_path.exists():
            queued.file_path.unlink()

        # Queue with new retry count
        new_path = self.queue_output(
            task_id=queued.task_id,
            tool_name=queued.tool_name,
            output=queued.output,
            status=queued.status,
            retry_count=new_retry_count,
        )

        return QueuedOutput.from_file(new_path)

    def get_queue_stats(self) -> dict[str, int]:
        """Get queue statistics."""
        pending = list(self.queue_dir.glob("*.json"))
        failed = list(self.failed_dir.glob("*.json"))

        due_count = 0
        for file_path in pending:
            queued = QueuedOutput.from_file(file_path)
            if queued and queued.is_due():
                due_count += 1

        return {
            "pending": len(pending),
            "due": due_count,
            "failed": len(failed),
        }


async def send_processed(
    api_client: Any,
    task_id: str,
    output_data: dict | str | None,
    status: str,
) -> tuple[bool, str]:
    """
    Send processed result to host.

    This is a shared utility function used by both ToolWorker and CLI retry.

    Args:
        api_client: API client with post() method
        task_id: Task identifier
        output_data: Output data or error message
        status: Task status (Processed/Failed)

    Returns:
        Tuple of (success: bool, error_message: str)
    """
    try:
        response = await api_client.post(
            f"/api/v2/document/Automation Task/{task_id}/method/processed",
            json={
                "data": {"output": output_data},
                "status": status,
            },
        )

        if response.status_code != 200:
            return False, f"HTTP {response.status_code}: {response.text[:100]}"

        return True, ""

    except Exception as e:
        return False, str(e)
