"""Tickets & Sprints — lightweight task management for multi-tool workflows.

Enables task tracking across different AI coding tools (VSCode/Roo Code,
aider, claude code, Windsurf, Cursor, etc.) via a REST API.

Data is stored in-memory with optional JSON persistence.
"""

from __future__ import annotations

import json
import time
import uuid
from enum import Enum
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field

from proxym.storage import SQLiteNamespaceStore, is_sqlite_database


# ── Enums ─────────────────────────────────────────────────────

class TicketStatus(str, Enum):
    BACKLOG = "backlog"
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    IN_REVIEW = "in_review"
    DONE = "done"
    CANCELLED = "cancelled"


class TicketPriority(str, Enum):
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"


class TicketType(str, Enum):
    TASK = "task"
    BUG = "bug"
    FEATURE = "feature"
    REFACTOR = "refactor"
    TEST = "test"
    DOCS = "docs"


class SprintStatus(str, Enum):
    PLANNING = "planning"
    ACTIVE = "active"
    COMPLETED = "completed"
    CANCELLED = "cancelled"


# ── Models ────────────────────────────────────────────────────

class ToolAssignment(BaseModel):
    """Which tool is assigned to work on a ticket."""
    tool: str  # vscode, aider, claude-code, windsurf, cursor, etc.
    agent_mode: str = ""  # code, architect, debug, ask
    model: str = ""  # model alias or full name
    started_at: float = 0
    completed_at: float = 0


class JobExecution(BaseModel):
    """Snapshot of the latest job execution for a ticket."""
    job_id: str = ""
    tool: str = ""
    mode: str = ""
    model: str = ""
    project_dir: str = ""
    prompt_excerpt: str = ""
    prompt_length: int = 0
    status: str = "queued"  # queued, running, done, failed, cancelled
    success: bool | None = None
    exit_code: int | None = None
    started_at: float = 0
    finished_at: float = 0
    duration_s: float = 0
    error: str = ""
    files_changed: list[str] = Field(default_factory=list)
    stdout_tail: str = ""  # last ~500 chars of output
    stderr_tail: str = ""  # last ~500 chars of error output
    stdout_lines: int = 0
    stderr_lines: int = 0
    execution_metadata: dict[str, Any] = Field(default_factory=dict)


class TicketComment(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
    author: str = ""  # tool name or user
    content: str = ""
    created_at: float = Field(default_factory=time.time)


class Ticket(BaseModel):
    id: str = Field(default_factory=lambda: f"TKT-{uuid.uuid4().hex[:6].upper()}")
    task: str = ""                                 # "co zrobić" — główne pole
    title: str = ""                                # auto-generated z task[:60] jeśli puste
    project_id: str | None = None                  # powiązanie z projektem
    description: str = ""
    status: TicketStatus = TicketStatus.TODO
    priority: TicketPriority = TicketPriority.MEDIUM
    type: TicketType = TicketType.TASK
    sprint_id: str = ""
    assigned_tool: ToolAssignment | None = None
    last_execution: JobExecution | None = None
    tags: list[str] = Field(default_factory=list)
    files: list[str] = Field(default_factory=list)  # affected file paths
    comments: list[TicketComment] = Field(default_factory=list)
    estimated_hours: float = 0
    actual_hours: float = 0
    created_at: float = Field(default_factory=time.time)
    updated_at: float = Field(default_factory=time.time)
    completed_at: float = 0

    def __init__(self, **data: Any) -> None:
        # Backward compat: if title provided but no task, use title as task
        if data.get("title") and not data.get("task"):
            data["task"] = data["title"]
        super().__init__(**data)
        # Auto-generate title from task if not provided
        if not self.title and self.task:
            truncated = self.task[:60].strip()
            if len(self.task) > 60:
                truncated += "\u2026"
            self.title = truncated

    def summary(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "task": self.task,
            "title": self.title,
            "project_id": self.project_id,
            "description": self.description,
            "status": self.status.value,
            "priority": self.priority.value,
            "type": self.type.value,
            "sprint_id": self.sprint_id,
            "assigned_tool": self.assigned_tool.model_dump() if self.assigned_tool else None,
            "last_job": self.last_execution.model_dump() if self.last_execution else None,
            "tags": self.tags,
            "files": self.files,
            "comments_count": len(self.comments),
            "estimated_hours": self.estimated_hours,
            "actual_hours": self.actual_hours,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "completed_at": self.completed_at,
        }


class Sprint(BaseModel):
    id: str = Field(default_factory=lambda: f"SPR-{uuid.uuid4().hex[:6].upper()}")
    name: str
    goal: str = ""
    status: SprintStatus = SprintStatus.PLANNING
    start_date: str = ""  # ISO date
    end_date: str = ""
    created_at: float = Field(default_factory=time.time)
    updated_at: float = Field(default_factory=time.time)

    def summary(self, tickets: list[Ticket] | None = None) -> dict[str, Any]:
        sprint_tickets = tickets or []
        total = len(sprint_tickets)
        done = sum(1 for t in sprint_tickets if t.status == TicketStatus.DONE)
        in_progress = sum(1 for t in sprint_tickets if t.status == TicketStatus.IN_PROGRESS)
        return {
            "id": self.id,
            "name": self.name,
            "goal": self.goal,
            "status": self.status.value,
            "start_date": self.start_date,
            "end_date": self.end_date,
            "tickets_total": total,
            "tickets_done": done,
            "tickets_in_progress": in_progress,
            "progress_pct": round(done / total * 100, 1) if total else 0,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
        }


# ── Manager ───────────────────────────────────────────────────

class TicketManager:
    """In-memory ticket & sprint store with optional JSON persistence."""

    def __init__(self, persist_path: Path | None = None):
        self.tickets: dict[str, Ticket] = {}
        self.sprints: dict[str, Sprint] = {}
        self._persist_path = persist_path
        self._store = SQLiteNamespaceStore(self._persist_path) if self._persist_path is not None else None
        if persist_path and persist_path.exists():
            self._load()

    # ── Tickets ───────────────────────────────────────────────

    def create_ticket(self, ticket: Ticket) -> Ticket:
        self.tickets[ticket.id] = ticket
        self._save()
        return ticket

    def get_ticket(self, ticket_id: str) -> Ticket | None:
        return self.tickets.get(ticket_id)

    def list_tickets(
        self,
        sprint_id: str | None = None,
        status: TicketStatus | None = None,
        priority: TicketPriority | None = None,
        tool: str | None = None,
        tag: str | None = None,
    ) -> list[Ticket]:
        """List tickets with optional filters."""
        result = self._apply_filters(
            list(self.tickets.values()),
            sprint_id=sprint_id,
            status=status,
            priority=priority,
            tool=tool,
            tag=tag,
        )
        return self._sort_tickets(result)

    def _apply_filters(
        self,
        tickets: list[Ticket],
        sprint_id: str | None = None,
        status: TicketStatus | None = None,
        priority: TicketPriority | None = None,
        tool: str | None = None,
        tag: str | None = None,
    ) -> list[Ticket]:
        """Apply filter criteria to ticket list."""
        result = tickets
        if sprint_id:
            result = [t for t in result if t.sprint_id == sprint_id]
        if status:
            result = [t for t in result if t.status == status]
        if priority:
            result = [t for t in result if t.priority == priority]
        if tool:
            result = [t for t in result if t.assigned_tool and t.assigned_tool.tool == tool]
        if tag:
            result = [t for t in result if tag in t.tags]
        return result

    def _sort_tickets(self, tickets: list[Ticket]) -> list[Ticket]:
        """Sort tickets by created_at descending."""
        return sorted(tickets, key=lambda t: t.created_at, reverse=True)

    def update_ticket(self, ticket_id: str, updates: dict[str, Any]) -> Ticket | None:
        ticket = self.tickets.get(ticket_id)
        if not ticket:
            return None
        for key, value in updates.items():
            if key == "status" and isinstance(value, str):
                value = TicketStatus(value)
            if key == "priority" and isinstance(value, str):
                value = TicketPriority(value)
            if key == "type" and isinstance(value, str):
                value = TicketType(value)
            if key == "assigned_tool" and isinstance(value, dict):
                value = ToolAssignment(**value)
            if key == "last_execution" and isinstance(value, dict):
                value = JobExecution(**value)
            if hasattr(ticket, key):
                setattr(ticket, key, value)
        ticket.updated_at = time.time()
        if ticket.status == TicketStatus.DONE and not ticket.completed_at:
            ticket.completed_at = time.time()
        self._save()
        return ticket

    def delete_ticket(self, ticket_id: str) -> bool:
        if ticket_id in self.tickets:
            del self.tickets[ticket_id]
            self._save()
            return True
        return False

    def add_comment(self, ticket_id: str, comment: TicketComment) -> bool:
        ticket = self.tickets.get(ticket_id)
        if not ticket:
            return False
        ticket.comments.append(comment)
        ticket.updated_at = time.time()
        self._save()
        return True

    def assign_tool(self, ticket_id: str, assignment: ToolAssignment) -> bool:
        ticket = self.tickets.get(ticket_id)
        if not ticket:
            return False
        assignment.started_at = time.time()
        ticket.assigned_tool = assignment
        if ticket.status == TicketStatus.TODO:
            ticket.status = TicketStatus.IN_PROGRESS
        ticket.updated_at = time.time()
        self._save()
        return True

    # ── Sprints ───────────────────────────────────────────────

    def create_sprint(self, sprint: Sprint) -> Sprint:
        self.sprints[sprint.id] = sprint
        self._save()
        return sprint

    def get_sprint(self, sprint_id: str) -> Sprint | None:
        return self.sprints.get(sprint_id)

    def list_sprints(self, status: SprintStatus | None = None) -> list[Sprint]:
        result = list(self.sprints.values())
        if status:
            result = [s for s in result if s.status == status]
        return sorted(result, key=lambda s: s.created_at, reverse=True)

    def update_sprint(self, sprint_id: str, updates: dict[str, Any]) -> Sprint | None:
        sprint = self.sprints.get(sprint_id)
        if not sprint:
            return None
        for key, value in updates.items():
            if key == "status" and isinstance(value, str):
                value = SprintStatus(value)
            if hasattr(sprint, key):
                setattr(sprint, key, value)
        sprint.updated_at = time.time()
        self._save()
        return sprint

    def delete_sprint(self, sprint_id: str) -> bool:
        if sprint_id in self.sprints:
            del self.sprints[sprint_id]
            self._save()
            return True
        return False

    def sprint_tickets(self, sprint_id: str) -> list[Ticket]:
        return [t for t in self.tickets.values() if t.sprint_id == sprint_id]

    def sprint_stats(self, sprint_id: str) -> dict[str, Any]:
        tickets = self.sprint_tickets(sprint_id)
        by_status: dict[str, int] = {}
        by_tool: dict[str, int] = {}
        total_estimated = 0.0
        total_actual = 0.0
        for t in tickets:
            by_status[t.status.value] = by_status.get(t.status.value, 0) + 1
            if t.assigned_tool:
                tool_name = t.assigned_tool.tool
                by_tool[tool_name] = by_tool.get(tool_name, 0) + 1
            total_estimated += t.estimated_hours
            total_actual += t.actual_hours
        return {
            "sprint_id": sprint_id,
            "total_tickets": len(tickets),
            "by_status": by_status,
            "by_tool": by_tool,
            "total_estimated_hours": round(total_estimated, 1),
            "total_actual_hours": round(total_actual, 1),
        }

    # ── Board view ────────────────────────────────────────────

    def board(self, sprint_id: str | None = None) -> dict[str, list[dict]]:
        """Kanban board view: columns by status."""
        tickets = self.list_tickets(sprint_id=sprint_id)
        columns: dict[str, list[dict]] = {s.value: [] for s in TicketStatus}
        for t in tickets:
            columns[t.status.value].append(t.summary())
        return columns

    # ── Persistence ───────────────────────────────────────────

    def _save(self):
        if not self._persist_path:
            return
        if self._store is None:
            self._store = SQLiteNamespaceStore(self._persist_path)
        self._store.save_namespaces({
            "tickets": {tid: t.model_dump() for tid, t in self.tickets.items()},
            "sprints": {sid: s.model_dump() for sid, s in self.sprints.items()},
        })

    def _load(self):
        if not self._persist_path or not self._persist_path.exists():
            return
        try:
            if is_sqlite_database(self._persist_path):
                tickets_data = self._store.load_namespace("tickets") if self._store else {}
                sprints_data = self._store.load_namespace("sprints") if self._store else {}
                for tid, tdata in tickets_data.items():
                    self.tickets[tid] = Ticket(**tdata)
                for sid, sdata in sprints_data.items():
                    self.sprints[sid] = Sprint(**sdata)
                return

            data = json.loads(self._persist_path.read_text())
            if isinstance(data, dict):
                for tid, tdata in data.get("tickets", {}).items():
                    self.tickets[tid] = Ticket(**tdata)
                for sid, sdata in data.get("sprints", {}).items():
                    self.sprints[sid] = Sprint(**sdata)
                if self.tickets or self.sprints:
                    self._save()
        except Exception:
            pass
