"""Tickets and sprints endpoints for dashboard API."""

from __future__ import annotations

from pathlib import Path
from typing import Annotated

from fastapi import APIRouter, Body, HTTPException
from pydantic import BaseModel

from proxym.dashboard._environments_api import get_tool_environment_snapshot
from proxym.dashboard.tickets import (
    JobExecution,
    Milestone,
    MilestoneStatus,
    Sprint,
    SprintStatus,
    Ticket,
    TicketComment,
    TicketManager,
    TicketPriority,
    TicketStatus,
    TicketType,
    ToolAssignment,
)
from proxym.dashboard.tools._helpers import _default_project_dir, _job_snapshot
from proxym.domain.events import (
    JobDispatched,
    TaskAssigned,
    TaskCreated,
    TaskDeleted,
    TaskUpdated,
)
from proxym.observability import log_call
from proxym.storage import DEFAULT_SQLITE_PATH
from proxym.tools import ToolRegistry
from proxym.tools.executor import JobStatus, ToolExecutor

router = APIRouter(tags=["tickets"])

TICKET_NOT_FOUND = "Ticket not found"
SPRINT_NOT_FOUND = "Sprint not found"

# Singleton
_ticket_mgr: TicketManager | None = None


def _tickets() -> TicketManager:
    global _ticket_mgr
    if _ticket_mgr is None:
        persist = DEFAULT_SQLITE_PATH
        _ticket_mgr = TicketManager(persist_path=persist)
    return _ticket_mgr


from proxym.dashboard.utils._emit_event import emit_event as _emit_event


def _ticket_has_active_job(ticket_id: str) -> bool:
    active_statuses = {JobStatus.QUEUED, JobStatus.RUNNING}
    return any(
        job.status in active_statuses for job in ToolExecutor.get().get_jobs_for_ticket(ticket_id)
    )


def _build_job_execution(job, environment_snapshot: dict[str, object] | None) -> JobExecution:
    env = environment_snapshot or {}
    raw_tool_instance = env.get("tool_instance")
    tool_instance = raw_tool_instance if isinstance(raw_tool_instance, dict) else {}
    return JobExecution(
        job_id=job.id,
        tool=job.tool_name,
        mode=job.mode,
        model=job.model,
        project_dir=job.project_dir,
        environment_id=str(env.get("id", "")),
        environment_name=str(env.get("name", "")),
        environment_kind=str(env.get("kind", "")),
        environment_target=str(env.get("target", "")),
        environment_tool_instance_name=str(env.get("tool_instance_name", "")),
        environment_tool_instance=tool_instance,
        prompt_excerpt=job.prompt[:400].strip(),
        prompt_length=len(job.prompt),
        status=job.status.value,
        success=None,
        exit_code=None,
    )


async def _enqueue_ticket_job(
    ticket: Ticket,
    tool_def,
    mode: str = "",
    model: str = "",
    instance: str = "",
    project_dir: str = "",
) -> tuple[object, JobExecution]:
    environment_snapshot = dict(get_tool_environment_snapshot(tool_def.name) or {})
    if instance:
        environment_snapshot["tool_instance_name"] = instance
        tool_instance = tool_def.get_instance(instance)
        if tool_instance:
            environment_snapshot["tool_instance"] = tool_instance.summary()

    resolved_project_dir = (
        project_dir
        or (environment_snapshot.get("path") if environment_snapshot else "")
        or _default_project_dir(ticket.project_id)
    )

    executor = ToolExecutor.get()
    await executor.start()
    job = executor.enqueue(
        ticket, tool_def, resolved_project_dir, mode, model, environment=environment_snapshot
    )
    execution = _build_job_execution(job, environment_snapshot)
    _tickets().update_ticket(ticket.id, {"last_execution": execution})
    return job, execution


async def _maybe_auto_dispatch_ticket(ticket_id: str) -> bool:
    mgr = _tickets()
    ticket = mgr.get_ticket(ticket_id)
    if not ticket or ticket.status != TicketStatus.IN_PROGRESS:
        return False

    assignment = ticket.assigned_tool
    if not assignment or assignment.tool == "human":
        return False

    if _ticket_has_active_job(ticket_id):
        return False

    tool_def = ToolRegistry.get().get_tool(assignment.tool)
    if not tool_def:
        return False

    try:
        await _enqueue_ticket_job(
            ticket,
            tool_def,
            mode=assignment.agent_mode or "",
            model=assignment.model or "",
            instance=assignment.instance or "",
        )
        return True
    except Exception as exc:
        import structlog

        structlog.get_logger().warning("auto_dispatch_failed", ticket=ticket.id, error=str(exc))
        return False


# ── Pydantic models ──────────────────────────────────────────


class CreateTicketReq(BaseModel):
    task: str = ""  # "co zrobić" — nowe główne pole
    title: str = ""  # backward compat
    project_id: str | None = None  # powiązanie z projektem
    description: str = ""
    status: str = "todo"
    priority: str = "medium"
    type: str = "task"
    sprint_id: str = ""
    tags: list[str] = []
    files: list[str] = []
    estimated_hours: float = 0
    tool: str | None = None  # od razu przypisz narzędzie


class UpdateTicketReq(BaseModel):
    task: str | None = None
    title: str | None = None
    project_id: str | None = None
    description: str | None = None
    status: str | None = None
    priority: str | None = None
    type: str | None = None
    sprint_id: str | None = None
    tags: list[str] | None = None
    files: list[str] | None = None
    estimated_hours: float | None = None
    actual_hours: float | None = None


class AssignToolReq(BaseModel):
    tool: str  # vscode, aider, claude-code, windsurf, cursor
    agent_mode: str = ""
    model: str = ""


class AddCommentReq(BaseModel):
    author: str = ""
    content: str


class CreateSprintReq(BaseModel):
    name: str
    goal: str = ""
    milestone_id: str = ""
    start_date: str = ""
    end_date: str = ""


class UpdateSprintReq(BaseModel):
    name: str | None = None
    goal: str | None = None
    milestone_id: str | None = None
    status: str | None = None
    start_date: str | None = None
    end_date: str | None = None


class CreateMilestoneReq(BaseModel):
    name: str
    goal: str = ""
    start_date: str = ""
    end_date: str = ""


class UpdateMilestoneReq(BaseModel):
    name: str | None = None
    goal: str | None = None
    status: str | None = None
    start_date: str | None = None
    end_date: str | None = None


# ── Ticket endpoints ─────────────────────────────────────────


@router.get("/tickets")
async def list_tickets(
    sprint_id: str | None = None,
    milestone_id: str | None = None,
    status: str | None = None,
    priority: str | None = None,
    tool: str | None = None,
    tag: str | None = None,
):
    """List tickets with optional filters."""
    mgr = _tickets()
    st = TicketStatus(status) if status else None
    pr = TicketPriority(priority) if priority else None
    tickets = mgr.list_tickets(
        sprint_id=sprint_id, milestone_id=milestone_id, status=st, priority=pr, tool=tool, tag=tag
    )
    return {"tickets": [mgr.ticket_overview(t) for t in tickets]}


@router.get("/tickets/export")
async def export_tickets(
    sprint_id: str | None = None,
    milestone_id: str | None = None,
    status: str | None = None,
    priority: str | None = None,
    tool: str | None = None,
    tag: str | None = None,
    view: str = "list",
    detail: bool = True,
):
    """Export ticket data for grouped dashboard views."""
    mgr = _tickets()
    st = TicketStatus(status) if status else None
    pr = TicketPriority(priority) if priority else None
    tickets = mgr.list_tickets(
        sprint_id=sprint_id, milestone_id=milestone_id, status=st, priority=pr, tool=tool, tag=tag
    )
    rows = []
    for ticket in tickets:
        row = mgr.ticket_detail(ticket) if detail else mgr.ticket_overview(ticket)
        if detail:
            row["job_history"] = [
                _job_snapshot(job) for job in ToolExecutor.get().get_jobs_for_ticket(ticket.id)
            ]
        rows.append(row)
    if view == "board":
        for row in rows:
            row["board_column"] = row.get("status", "")
    return {
        "tickets": rows,
        "count": len(rows),
        "view": view,
        "detail": detail,
        "filters": {
            "sprint_id": sprint_id,
            "milestone_id": milestone_id,
            "status": status,
            "priority": priority,
            "tool": tool,
            "tag": tag,
        },
    }


@router.post("/tickets")
@log_call()
async def create_ticket(
    task: str = Body(""),
    title: str = Body(""),
    project_id: str | None = Body(None),
    description: str = Body(""),
    status: str = Body("todo"),
    priority: str = Body("medium"),
    type: str = Body("task"),
    sprint_id: str = Body(""),
    milestone_id: str = Body(""),
    tags: list[str] = Body(default_factory=list),
    files: list[str] = Body(default_factory=list),
    context_files: list[str] = Body(default_factory=list),
    depends_on: list[str] = Body(default_factory=list),
    estimated_hours: float = Body(0),
    tool: str | None = Body(None),
):
    """Create a new ticket. Accepts 'task' (new) or 'title' (legacy)."""
    task_text = task or title
    if not task_text:
        from fastapi import HTTPException

        raise HTTPException(400, "Provide 'task' or 'title'")
    ticket = Ticket(
        task=task_text,
        title=title,  # auto-generated from task if empty
        project_id=project_id,
        description=description,
        status=TicketStatus(status),
        priority=TicketPriority(priority),
        type=TicketType(type),
        sprint_id=sprint_id,
        tags=tags,
        files=files,
        context_files=context_files,
        depends_on=depends_on,
        estimated_hours=estimated_hours,
    )
    mgr = _tickets()
    created = mgr.create_ticket(ticket)

    _emit_event(
        TaskCreated(
            aggregate_id=created.id,
            actor="api",
            data={"task": task_text, "project_id": project_id or "", "priority": priority},
        )
    )

    # Od razu przypisz narzędzie jeśli podane
    if tool:
        assignment = ToolAssignment(tool=tool, instance="")
        mgr.assign_tool(created.id, assignment)

        tool_def = None
        try:
            from proxym.tools import ToolRegistry

            tool_def = ToolRegistry.get().get_tool(tool)
        except Exception:
            tool_def = None

        if tool_def and tool_def.name == "human":
            return mgr.get_ticket(created.id).summary()

        # Auto-dispatch: enqueue job immediately so user doesn't need a second step
        try:
            from proxym.dashboard.tickets import JobExecution
            from proxym.tools.executor import ToolExecutor

            if tool_def:
                executor = ToolExecutor.get()
                environment_snapshot = get_tool_environment_snapshot(tool_def.name)
                project_dir = (
                    environment_snapshot.get("path") if environment_snapshot else ""
                ) or _default_project_dir(project_id)
                job = executor.enqueue(
                    created, tool_def, project_dir, environment=environment_snapshot
                )
                # Store execution snapshot on the ticket (persists to JSON)
                created.last_execution = JobExecution(
                    job_id=job.id,
                    tool=job.tool_name,
                    mode=job.mode,
                    model=job.model,
                    project_dir=job.project_dir,
                    environment_id=environment_snapshot.get("id", "")
                    if environment_snapshot
                    else "",
                    environment_name=environment_snapshot.get("name", "")
                    if environment_snapshot
                    else "",
                    environment_kind=environment_snapshot.get("kind", "")
                    if environment_snapshot
                    else "",
                    environment_target=environment_snapshot.get("target", "")
                    if environment_snapshot
                    else "",
                    environment_tool_instance_name=environment_snapshot.get(
                        "tool_instance_name", ""
                    )
                    if environment_snapshot
                    else "",
                    environment_tool_instance=environment_snapshot.get("tool_instance", {})
                    if environment_snapshot
                    else {},
                    prompt_excerpt=job.prompt[:400].strip(),
                    prompt_length=len(job.prompt),
                    status=job.status.value,
                    success=None,
                    exit_code=None,
                )
                mgr.update_ticket(created.id, {"last_execution": created.last_execution})
        except Exception as exc:
            import structlog

            structlog.get_logger().warning(
                "auto_dispatch_failed", ticket=created.id, error=str(exc)
            )

    return created.summary()


@router.get("/tickets/board/view")
async def ticket_board(sprint_id: str | None = None, milestone_id: str | None = None):
    """Kanban board view: tickets grouped by status columns."""
    return _tickets().board(sprint_id=sprint_id, milestone_id=milestone_id)


@router.post("/tickets/dedup")
@log_call()
async def deduplicate_tickets(dry_run: bool = Body(True, embed=True)):
    """Find and remove duplicate tickets.

    Duplicates are tickets with identical (task, project_id, type).
    The ticket with the most activity (comments, tool assignment, latest
    update) is kept; the rest are removed.

    Set ``dry_run=false`` to actually delete the duplicates.
    """
    result = _tickets().find_duplicates(dry_run=dry_run)
    return result


@router.get("/tickets/{ticket_id}")
async def get_ticket(ticket_id: str):
    """Get ticket details including comments."""
    ticket = _tickets().get_ticket(ticket_id)
    if not ticket:
        raise HTTPException(404, TICKET_NOT_FOUND)
    result = _tickets().ticket_detail(ticket)
    result["job_history"] = [
        _job_snapshot(job) for job in ToolExecutor.get().get_jobs_for_ticket(ticket_id)
    ]
    return result


@router.put("/tickets/{ticket_id}")
async def update_ticket(
    ticket_id: str,
    task: str | None = Body(None),
    title: str | None = Body(None),
    project_id: str | None = Body(None),
    description: str | None = Body(None),
    status: str | None = Body(None),
    priority: str | None = Body(None),
    type: str | None = Body(None),
    sprint_id: str | None = Body(None),
    milestone_id: str | None = Body(None),
    tags: list[str] | None = Body(None),
    files: list[str] | None = Body(None),
    context_files: list[str] | None = Body(None),
    depends_on: list[str] | None = Body(None),
    estimated_hours: float | None = Body(None),
    actual_hours: float | None = Body(None),
):
    """Update a ticket's fields."""
    existing = _tickets().get_ticket(ticket_id)
    previous_status = existing.status if existing else None
    updates = {
        k: v
        for k, v in {
            "task": task,
            "title": title,
            "project_id": project_id,
            "description": description,
            "status": status,
            "priority": priority,
            "type": type,
            "sprint_id": sprint_id,
            "tags": tags,
            "files": files,
            "context_files": context_files,
            "depends_on": depends_on,
            "estimated_hours": estimated_hours,
            "actual_hours": actual_hours,
        }.items()
        if v is not None
    }
    ticket = _tickets().update_ticket(ticket_id, updates)
    if not ticket:
        raise HTTPException(404, TICKET_NOT_FOUND)

    _emit_event(
        TaskUpdated(
            aggregate_id=ticket_id,
            actor="api",
            data={k: str(v) for k, v in updates.items()},
        )
    )

    if previous_status != TicketStatus.IN_PROGRESS and ticket.status == TicketStatus.IN_PROGRESS:
        await _maybe_auto_dispatch_ticket(ticket_id)

    return ticket.summary()


@router.delete("/tickets/{ticket_id}")
async def delete_ticket(ticket_id: str):
    """Delete a ticket."""
    from proxym.dashboard.utils.delete_ticket import delete_item

    result = delete_item(_tickets().delete_ticket, ticket_id, TICKET_NOT_FOUND)
    _emit_event(TaskDeleted(aggregate_id=ticket_id, actor="api"))
    return result


@router.post("/tickets/{ticket_id}/assign")
@log_call()
async def assign_ticket_tool(
    ticket_id: str,
    tool: str = Body(...),
    agent_mode: str = Body(""),
    model: str = Body(""),
    instance: str = Body(""),
):
    """Assign a tool (vscode, aider, claude-code, etc.) to work on a ticket."""
    assignment = ToolAssignment(tool=tool, agent_mode=agent_mode, model=model, instance=instance)
    existing = _tickets().get_ticket(ticket_id)
    was_in_progress = existing and existing.status == TicketStatus.IN_PROGRESS
    if _tickets().assign_tool(ticket_id, assignment):
        _emit_event(
            TaskAssigned(
                aggregate_id=ticket_id,
                actor="api",
                data={"tool": tool, "agent_mode": agent_mode, "model": model},
            )
        )
        if was_in_progress:
            await _maybe_auto_dispatch_ticket(ticket_id)
        return {"ok": True, "ticket": _tickets().get_ticket(ticket_id).summary()}
    raise HTTPException(404, TICKET_NOT_FOUND)


@router.post("/tickets/{ticket_id}/comment")
async def add_ticket_comment(
    ticket_id: str,
    author: str = Body(""),
    content: str = Body(...),
):
    """Add a comment to a ticket (from any tool or user)."""
    comment = TicketComment(author=author, content=content)
    if _tickets().add_comment(ticket_id, comment):
        return {"ok": True}
    raise HTTPException(404, TICKET_NOT_FOUND)


# ── Sprint endpoints ─────────────────────────────────────────


@router.get("/sprints")
async def list_sprints(status: str | None = None, milestone_id: str | None = None):
    """List sprints."""
    st = SprintStatus(status) if status else None
    sprints = _tickets().list_sprints(status=st, milestone_id=milestone_id)
    mgr = _tickets()
    return {"sprints": [mgr.sprint_overview(s) for s in sprints]}


@router.post("/sprints")
async def create_sprint(req: CreateSprintReq):
    """Create a new sprint."""
    sprint = Sprint(
        name=req.name,
        goal=req.goal,
        milestone_id=req.milestone_id,
        start_date=req.start_date,
        end_date=req.end_date,
    )
    created = _tickets().create_sprint(sprint)
    return _tickets().sprint_overview(created)


@router.get("/sprints/{sprint_id}")
async def get_sprint(sprint_id: str):
    """Get sprint details with ticket stats."""
    mgr = _tickets()
    sprint = mgr.get_sprint(sprint_id)
    if not sprint:
        raise HTTPException(404, SPRINT_NOT_FOUND)
    result = mgr.sprint_overview(sprint)
    result["stats"] = mgr.sprint_stats(sprint_id)
    return result


@router.put("/sprints/{sprint_id}")
async def update_sprint(sprint_id: str, req: UpdateSprintReq):
    """Update sprint fields."""
    updates = {k: v for k, v in req.model_dump().items() if v is not None}
    sprint = _tickets().update_sprint(sprint_id, updates)
    if not sprint:
        raise HTTPException(404, SPRINT_NOT_FOUND)
    mgr = _tickets()
    return mgr.sprint_overview(sprint)


@router.delete("/sprints/{sprint_id}")
async def delete_sprint(sprint_id: str):
    """Delete a sprint."""
    from proxym.dashboard.utils.delete_ticket import delete_item

    return delete_item(_tickets().delete_sprint, sprint_id, SPRINT_NOT_FOUND)


# ── Milestone endpoints ──────────────────────────────────────


@router.get("/milestones")
async def list_milestones(status: str | None = None):
    """List milestones."""
    st = MilestoneStatus(status) if status else None
    mgr = _tickets()
    milestones = mgr.list_milestones(status=st)
    return {"milestones": [mgr.milestone_overview(m) for m in milestones]}


@router.post("/milestones")
async def create_milestone(req: CreateMilestoneReq):
    """Create a new milestone."""
    milestone = Milestone(
        name=req.name,
        goal=req.goal,
        start_date=req.start_date,
        end_date=req.end_date,
    )
    created = _tickets().create_milestone(milestone)
    return _tickets().milestone_overview(created)


@router.get("/milestones/{milestone_id}")
async def get_milestone(milestone_id: str):
    """Get milestone details with ticket stats."""
    mgr = _tickets()
    milestone = mgr.get_milestone(milestone_id)
    if not milestone:
        raise HTTPException(404, "Milestone not found")
    result = mgr.milestone_overview(milestone)
    result["stats"] = mgr.milestone_stats(milestone_id)
    return result


@router.put("/milestones/{milestone_id}")
async def update_milestone(milestone_id: str, req: UpdateMilestoneReq):
    """Update milestone fields."""
    updates = {k: v for k, v in req.model_dump().items() if v is not None}
    milestone = _tickets().update_milestone(milestone_id, updates)
    if not milestone:
        raise HTTPException(404, "Milestone not found")
    mgr = _tickets()
    return mgr.milestone_overview(milestone)


@router.delete("/milestones/{milestone_id}")
async def delete_milestone(milestone_id: str):
    """Delete a milestone."""
    from proxym.dashboard.utils.delete_milestone import delete_milestone as delete_milestone_util

    return delete_milestone_util(_tickets().delete_milestone, milestone_id)
