"""Ticket Orchestrator — automatic status management with LLM analysis.

Listens for job completion events and orchestrates ticket status transitions:
TODO → IN_PROGRESS → IN_REVIEW → DONE

Adds LLM-generated comments as audit trail for each transition.
"""

from __future__ import annotations

import asyncio
import json
import time
from typing import Any

import structlog
from litellm import acompletion

from proxym.config import get_settings
from proxym.dashboard.tickets import Ticket, TicketStatus, TicketComment
from proxym.dashboard._lifecycle import LifecycleMixin
from proxym.domain.events import JobCompleted, JobFailed
from proxym.domain.setup import get_event_store
from proxym.observability import log_call

logger = structlog.get_logger()


class TicketOrchestrator(LifecycleMixin):
    """Orchestrates ticket status transitions based on job events."""
    
    _logger_name = "ticket_orchestrator"
    
    def __init__(self) -> None:
        super().__init__()
        self._event_store = get_event_store()
    async def _event_loop(self) -> None:
        """Main event processing loop."""
        logger.info("ticket_orchestrator_loop_started")
        while self._running:
            try:
                # Get events since last check
                events = self._event_store.get_events(since="last_orchestrator_run")
                if events:
                    logger.info("ticket_orchestrator_events_found", count=len(events), types=[e.event_type for e in events])
                for event in events:
                    if isinstance(event, (JobCompleted, JobFailed)):
                        logger.info("ticket_orchestrator_processing_job_event", event_type=event.event_type, ticket_id=event.data.get("ticket_id"))
                        await self._handle_job_event(event)
                    else:
                        logger.debug("ticket_orchestrator_skipping_event", event_type=event.event_type)
                # Mark events as processed
                self._event_store.mark_processed("orchestrator", events)
                await asyncio.sleep(2.0)  # Poll every 2 seconds
            except asyncio.CancelledError:
                raise
            except Exception as exc:
                logger.error("orchestrator_event_loop_error", error=str(exc))
                await asyncio.sleep(5.0)
    
    @log_call(level="info", slow_threshold_ms=5000)
    async def _handle_job_event(self, event: JobCompleted | JobFailed) -> None:
        """Handle a job completion event."""
        ticket_id = event.data.get("ticket_id")
        if not ticket_id:
            return
        
        try:
            from proxym.dashboard._tickets_api import _tickets
            mgr = _tickets()
            ticket = mgr.get_ticket(ticket_id)
            if not ticket:
                return

            if isinstance(event, JobCompleted):
                await self._handle_completed_job(mgr, ticket, event)
            else:
                await self._handle_failed_job(mgr, ticket, event)

        except Exception as exc:
            logger.error("handle_job_event_failed", ticket_id=ticket_id, error=str(exc))

    async def _handle_completed_job(self, mgr: Any, ticket: Ticket, event: JobCompleted) -> None:
        """Handle a successful job using a transparent review flow."""
        review = await self._review_job(ticket, event, success=True)
        summary_comment = review["summary_comment"]
        decision_comment = review["decision_comment"]
        final_status = review["final_status"]

        # First make the review visible to humans.
        mgr.add_comment(ticket.id, TicketComment(author="orchestrator", content=summary_comment))

        before_review = ticket.status
        if before_review not in {TicketStatus.IN_REVIEW, TicketStatus.DONE, TicketStatus.CANCELLED}:
            mgr.update_ticket(ticket.id, {"status": TicketStatus.IN_REVIEW.value})
            self._emit_status_change(
                ticket,
                before_review,
                TicketStatus.IN_REVIEW,
                event,
                phase="review_started",
                comment=summary_comment,
                verdict="in_review",
            )

        # Emit a second, explicit decision when the LLM is confident enough.
        if decision_comment:
            mgr.add_comment(ticket.id, TicketComment(author="orchestrator", content=decision_comment))

        if final_status != TicketStatus.IN_REVIEW and final_status != ticket.status:
            before_final = ticket.status
            mgr.update_ticket(ticket.id, {"status": final_status.value})
            self._emit_status_change(
                ticket,
                before_final,
                final_status,
                event,
                phase="review_completed",
                comment=decision_comment or summary_comment,
                verdict=final_status.value,
            )

    async def _handle_failed_job(self, mgr: Any, ticket: Ticket, event: JobFailed) -> None:
        """Handle a failed job with a transparent review comment and retry hint."""
        review = await self._review_job(ticket, event, success=False)
        summary_comment = review["summary_comment"]
        decision_comment = review["decision_comment"]
        final_status = review["final_status"]

        mgr.add_comment(ticket.id, TicketComment(author="orchestrator", content=summary_comment))
        if decision_comment:
            mgr.add_comment(ticket.id, TicketComment(author="orchestrator", content=decision_comment))

        if final_status != ticket.status:
            before_final = ticket.status
            mgr.update_ticket(ticket.id, {"status": final_status.value})
            self._emit_status_change(
                ticket,
                before_final,
                final_status,
                event,
                phase="failure_review",
                comment=decision_comment or summary_comment,
                verdict=final_status.value,
            )

    async def _review_job(self, ticket: Ticket, event: JobCompleted | JobFailed, *, success: bool) -> dict[str, Any]:
        """Ask the LLM for a transparent review and normalise the result."""
        prompt = self._build_review_prompt(ticket, event, success=success)
        payload: dict[str, Any] = {}

        try:
            response = await acompletion(
                model=self._review_model(),
                messages=[{"role": "user", "content": prompt}],
                max_tokens=350,
                temperature=0.2,
            )
            content = response.choices[0].message.content.strip()
            if content.startswith("```"):
                content = content.split("\n", 1)[1].rsplit("```", 1)[0].strip()
            payload = json.loads(content)
        except Exception as exc:
            logger.debug("ticket_review_llm_failed", ticket_id=ticket.id, error=str(exc))

        return self._normalise_review_payload(ticket, event, payload, success=success)

    @staticmethod
    def _review_model() -> str:
        settings = get_settings()
        for candidate in (settings.fallback_model, settings.default_model):
            try:
                resolved = settings.resolve_model_alias(candidate)
                if resolved:
                    return resolved
            except Exception:
                continue
        return "anthropic/claude-haiku-4-5-20251001"

    @staticmethod
    def _temporary_error(error: str) -> bool:
        temporary_errors = ["timeout", "network", "rate limit", "quota"]
        lowered = error.lower()
        return any(err in lowered for err in temporary_errors)

    def _build_review_prompt(self, ticket: Ticket, event: JobCompleted | JobFailed, *, success: bool) -> str:
        execution = ticket.last_execution
        files_changed = getattr(execution, "files_changed", []) or []
        stdout_tail = getattr(execution, "stdout_tail", "") or ""
        stderr_tail = getattr(execution, "stderr_tail", "") or ""
        duration_s = event.data.get("duration_s", getattr(execution, "duration_s", 0) or 0)
        error = event.data.get("error", "") if isinstance(event, JobFailed) else ""
        return f"""You are the Proxym ticket review orchestrator.

Your job is to create a transparent, human-readable review trail and choose the next status.

Return ONLY valid JSON with these keys:
- summary_comment: a concise audit comment for the ticket history
- decision_comment: a concise comment explaining the final review decision
- final_status: one of done, in_review, in_progress, todo

Rules:
- This is a sequential flow: a successful job should first enter in_review.
- Do NOT treat files_changed = 0 as a failure by itself. Read-only and analysis-only tasks may still be done.
- If the evidence is incomplete, keep the ticket in_review.
- If the work is clearly complete, final_status should be done.
- For failures, prefer in_progress for transient problems (timeout, network, rate limit, quota) and todo otherwise.

Ticket:
- id: {ticket.id}
- task: {ticket.task}
- title: {ticket.title}
- type: {ticket.type.value}
- current_status: {ticket.status.value}
- tool: {getattr(ticket.assigned_tool, 'tool', '') if ticket.assigned_tool else ''}
- files: {', '.join(ticket.files) if ticket.files else '(none)'}
- context_files: {', '.join(ticket.context_files) if ticket.context_files else '(none)'}

Execution:
- success: {str(success).lower()}
- duration_s: {duration_s}
- files_changed: {len(files_changed)}
- changed_files: {', '.join(files_changed[:20]) if files_changed else '(none)'}
- stdout_tail: {stdout_tail[-800:] if stdout_tail else '(empty)'}
- stderr_tail: {stderr_tail[-800:] if stderr_tail else '(empty)'}
- error: {error or '(none)'}

Reply with JSON only. No markdown, no code fences."""

    def _normalise_review_payload(
        self,
        ticket: Ticket,
        event: JobCompleted | JobFailed,
        payload: dict[str, Any],
        *,
        success: bool,
    ) -> dict[str, Any]:
        execution = ticket.last_execution
        files_changed = getattr(execution, "files_changed", []) or []
        changed_count = len(files_changed)
        error = event.data.get("error", "") if isinstance(event, JobFailed) else ""

        summary_comment = str(payload.get("summary_comment") or "").strip()
        decision_comment = str(payload.get("decision_comment") or "").strip()
        final_status_raw = str(payload.get("final_status") or "").strip().lower()

        if not summary_comment:
            summary_comment = self._fallback_summary_comment(ticket, event, success=success, files_changed=changed_count)
        if not decision_comment:
            decision_comment = self._fallback_decision_comment(ticket, event, success=success, files_changed=changed_count)

        if success:
            default_status = TicketStatus.IN_REVIEW
            if final_status_raw == "done":
                final_status = TicketStatus.DONE
            elif final_status_raw == "todo":
                final_status = TicketStatus.TODO
            elif final_status_raw == "in_progress":
                final_status = TicketStatus.IN_PROGRESS
            else:
                final_status = default_status
        else:
            if final_status_raw == "in_progress":
                final_status = TicketStatus.IN_PROGRESS
            elif final_status_raw == "done":
                final_status = TicketStatus.DONE
            elif final_status_raw == "in_review":
                final_status = TicketStatus.IN_REVIEW
            else:
                final_status = TicketStatus.IN_PROGRESS if self._temporary_error(error) else TicketStatus.TODO

        return {
            "summary_comment": summary_comment,
            "decision_comment": decision_comment,
            "final_status": final_status,
            "files_changed": files_changed,
        }

    def _fallback_summary_comment(
        self,
        ticket: Ticket,
        event: JobCompleted | JobFailed,
        *,
        success: bool,
        files_changed: int,
    ) -> str:
        tool_name = getattr(ticket.assigned_tool, "tool", "orchestrator") if ticket.assigned_tool else "orchestrator"
        if success:
            return (
                f"[LLM review] Job completed for '{ticket.title or ticket.task}'. "
                f"Tool: {tool_name}. Files changed: {files_changed}. Entering review."
            )
        error = event.data.get("error", "") if isinstance(event, JobFailed) else ""
        return (
            f"[LLM review] Job failed for '{ticket.title or ticket.task}'. "
            f"Tool: {tool_name}. Error: {error[:160] or 'unknown error'}."
        )

    def _fallback_decision_comment(
        self,
        ticket: Ticket,
        event: JobCompleted | JobFailed,
        *,
        success: bool,
        files_changed: int,
    ) -> str:
        if success:
            if files_changed == 0:
                return (
                    f"[LLM review] No file changes were recorded, but that can still be valid for read-only work. "
                    f"Keeping '{ticket.title or ticket.task}' in review until the result is confirmed."
                )
            return f"[LLM review] Work looks ready for final verification on '{ticket.title or ticket.task}'."

        error = event.data.get("error", "") if isinstance(event, JobFailed) else ""
        if self._temporary_error(error):
            return f"[LLM review] Failure looks transient; keeping '{ticket.title or ticket.task}' in progress for retry."
        return f"[LLM review] Failure needs another implementation pass on '{ticket.title or ticket.task}'."

    def _emit_status_change(
        self,
        ticket: Ticket,
        old_status: TicketStatus,
        new_status: TicketStatus,
        trigger_event: JobCompleted | JobFailed,
        *,
        phase: str = "",
        comment: str = "",
        verdict: str = "",
    ) -> None:
        """Emit a TicketStatusChanged domain event."""
        try:
            from proxym.domain.events import TicketStatusChanged

            execution = ticket.last_execution
            
            self._event_store.append(TicketStatusChanged(
                aggregate_id=ticket.id,
                actor="orchestrator",
                data={
                    "ticket_id": ticket.id,
                    "old_status": old_status.value,
                    "new_status": new_status.value,
                    "phase": phase,
                    "comment": comment,
                    "verdict": verdict,
                    "files_changed": len(getattr(execution, "files_changed", []) or []),
                    "trigger_event": trigger_event.__class__.__name__,
                    "trigger_job_id": trigger_event.aggregate_id,
                    "timestamp": time.time(),
                },
            ))
        except Exception as exc:
            logger.debug("emit_status_change_failed", error=str(exc))
