"""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


# ── 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 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
    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,
            "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
        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 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
        try:
            self._persist_path.parent.mkdir(parents=True, exist_ok=True)
            data = {
                "tickets": {tid: t.model_dump() for tid, t in self.tickets.items()},
                "sprints": {sid: s.model_dump() for sid, s in self.sprints.items()},
            }
            self._persist_path.write_text(json.dumps(data, default=str, indent=2))
        except Exception:
            pass

    def _load(self):
        try:
            data = json.loads(self._persist_path.read_text())
            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)
        except Exception:
            pass
