"""TicketManager — in-memory ticket & sprint store with CRUD operations."""

from __future__ import annotations

import time
from typing import Any

from proxym.dashboard.tickets._models import (
    JobExecution,
    Milestone,
    MilestoneStatus,
    Sprint,
    SprintStatus,
    Ticket,
    TicketComment,
    TicketPriority,
    TicketStatus,
    TicketType,
    ToolAssignment,
)


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

    def __init__(self, persist_path=None):
        from pathlib import Path

        self.tickets: dict[str, Ticket] = {}
        self.sprints: dict[str, Sprint] = {}
        self.milestones: dict[str, Milestone] = {}
        self._persist_path: Path | None = persist_path
        self._store = None
        if self._persist_path is not None:
            from proxym.storage import SQLiteNamespaceStore

            self._store = SQLiteNamespaceStore(self._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,
        milestone_id: str | None = None,
        status: TicketStatus | None = None,
        priority: TicketPriority | None = None,
        tool: str | None = None,
        tag: str | None = None,
        project_id: str | set[str] | None = None,
    ) -> list[Ticket]:
        """List tickets with optional filters."""
        result = self._apply_filters(
            list(self.tickets.values()),
            sprint_id=sprint_id,
            milestone_id=milestone_id,
            status=status,
            priority=priority,
            tool=tool,
            tag=tag,
            project_id=project_id,
        )
        return self._sort_tickets(result)

    def _apply_filters(
        self,
        tickets: list[Ticket],
        sprint_id: str | None = None,
        milestone_id: str | None = None,
        status: TicketStatus | None = None,
        priority: TicketPriority | None = None,
        tool: str | None = None,
        tag: str | None = None,
        project_id: str | set[str] | None = None,
    ) -> list[Ticket]:
        """Apply filter criteria to ticket list."""
        result = tickets
        if project_id:
            if isinstance(project_id, set):
                result = [t for t in result if t.project_id in project_id]
            else:
                result = [t for t in result if t.project_id == project_id]
        if sprint_id:
            result = [t for t in result if t.sprint_id == sprint_id]
        if milestone_id:
            sprint_ids = {s.id for s in self.sprints.values() if s.milestone_id == milestone_id}
            result = [t for t in result if t.sprint_id in sprint_ids]
        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
        if "last_job" in updates and "last_execution" not in updates:
            updates = {**updates, "last_execution": updates["last_job"]}
        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 key == "comments" and isinstance(value, list):
                value = [
                    TicketComment(**item) if isinstance(item, dict) else item
                    for item in 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:
        from proxym.dashboard.utils.delete_ticket import delete_ticket_from_dict

        return delete_ticket_from_dict(self.tickets, ticket_id, self._save)

    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()
        try:
            from proxym.dashboard.utils._emit_event import emit_event
            from proxym.domain.events import TaskCommentAdded

            emit_event(
                TaskCommentAdded(
                    aggregate_id=ticket_id,
                    actor=comment.author or "dashboard",
                    data={
                        "comment_id": comment.id,
                        "author": comment.author,
                        "content": comment.content,
                    },
                )
            )
        except Exception:
            pass
        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

    def ticket_context(self, ticket: Ticket) -> dict[str, Any]:
        sprint = self.get_sprint(ticket.sprint_id) if ticket.sprint_id else None
        milestone = (
            self.get_milestone(sprint.milestone_id) if sprint and sprint.milestone_id else None
        )
        project_name = ""
        project_path = ""
        if ticket.project_id:
            try:
                from proxym.dashboard._projects_api import _get_registry

                project = _get_registry().get(ticket.project_id)
                if project:
                    project_name = project.name or ""
                    project_path = project.local_path or project.source_path or ""
            except Exception:
                pass
        return {
            "project_name": project_name,
            "project_path": project_path,
            "sprint_name": sprint.name if sprint else "",
            "sprint_status": sprint.status.value if sprint else "",
            "milestone_id": sprint.milestone_id if sprint else "",
            "milestone_name": milestone.name if milestone else "",
            "milestone_status": milestone.status.value if milestone else "",
        }

    def ticket_overview(self, ticket: Ticket) -> dict[str, Any]:
        data = ticket.summary()
        data.update(self.ticket_context(ticket))
        return data

    def ticket_detail(self, ticket: Ticket) -> dict[str, Any]:
        data = self.ticket_overview(ticket)
        data["comments"] = [comment.model_dump() for comment in ticket.comments]
        return data

    def sprint_overview(self, sprint: Sprint) -> dict[str, Any]:
        data = sprint.summary(self.sprint_tickets(sprint.id))
        milestone = self.get_milestone(sprint.milestone_id) if sprint.milestone_id else None
        data["milestone_name"] = milestone.name if milestone else ""
        data["milestone_status"] = milestone.status.value if milestone else ""
        return data

    def milestone_overview(self, milestone: Milestone) -> dict[str, Any]:
        return milestone.summary(
            self.milestone_sprints(milestone.id), self.milestone_tickets(milestone.id)
        )

    # ── Milestones ────────────────────────────────────────────

    def create_milestone(self, milestone: Milestone) -> Milestone:
        self.milestones[milestone.id] = milestone
        self._save()
        return milestone

    def get_milestone(self, milestone_id: str) -> Milestone | None:
        return self.milestones.get(milestone_id)

    def list_milestones(self, status: MilestoneStatus | None = None) -> list[Milestone]:
        result = list(self.milestones.values())
        if status:
            result = [m for m in result if m.status == status]
        return sorted(result, key=lambda m: m.created_at, reverse=True)

    def update_milestone(self, milestone_id: str, updates: dict[str, Any]) -> Milestone | None:
        milestone = self.milestones.get(milestone_id)
        if not milestone:
            return None
        for key, value in updates.items():
            if key == "status" and isinstance(value, str):
                value = MilestoneStatus(value)
            if hasattr(milestone, key):
                setattr(milestone, key, value)
        milestone.updated_at = time.time()
        self._save()
        return milestone

    def delete_milestone(self, milestone_id: str) -> bool:
        if milestone_id in self.milestones:
            del self.milestones[milestone_id]
            for sprint in self.sprints.values():
                if sprint.milestone_id == milestone_id:
                    sprint.milestone_id = ""
                    sprint.updated_at = time.time()
            self._save()
            return True
        return False

    def milestone_sprints(self, milestone_id: str) -> list[Sprint]:
        return sorted(
            [s for s in self.sprints.values() if s.milestone_id == milestone_id],
            key=lambda s: s.created_at,
            reverse=True,
        )

    def milestone_tickets(self, milestone_id: str) -> list[Ticket]:
        sprint_ids = {s.id for s in self.sprints.values() if s.milestone_id == milestone_id}
        return [t for t in self.tickets.values() if t.sprint_id in sprint_ids]

    def milestone_stats(self, milestone_id: str) -> dict[str, Any]:
        sprints = self.milestone_sprints(milestone_id)
        tickets = self.milestone_tickets(milestone_id)
        sprint_status_counts: dict[str, int] = {}
        by_status: dict[str, int] = {}
        by_tool: dict[str, int] = {}
        total_estimated = 0.0
        total_actual = 0.0
        for sprint in sprints:
            sprint_status_counts[sprint.status.value] = (
                sprint_status_counts.get(sprint.status.value, 0) + 1
            )
        for ticket in tickets:
            by_status[ticket.status.value] = by_status.get(ticket.status.value, 0) + 1
            if ticket.assigned_tool:
                tool_name = ticket.assigned_tool.tool
                by_tool[tool_name] = by_tool.get(tool_name, 0) + 1
            total_estimated += ticket.estimated_hours
            total_actual += ticket.actual_hours
        completed_sprints = sprint_status_counts.get(SprintStatus.COMPLETED.value, 0)
        return {
            "milestone_id": milestone_id,
            "total_sprints": len(sprints),
            "total_tickets": len(tickets),
            "sprint_status_counts": sprint_status_counts,
            "by_status": by_status,
            "by_tool": by_tool,
            "tickets_done": by_status.get(TicketStatus.DONE.value, 0),
            "tickets_in_progress": by_status.get(TicketStatus.IN_PROGRESS.value, 0),
            "sprints_completed": completed_sprints,
            "total_estimated_hours": round(total_estimated, 1),
            "total_actual_hours": round(total_actual, 1),
            "progress_pct": round(completed_sprints / len(sprints) * 100, 1) if sprints else 0,
        }

    # ── 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, milestone_id: str | None = None
    ) -> list[Sprint]:
        result = list(self.sprints.values())
        if status:
            result = [s for s in result if s.status == status]
        if milestone_id:
            result = [s for s in result if s.milestone_id == milestone_id]
        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]
            for ticket in self.tickets.values():
                if ticket.sprint_id == sprint_id:
                    ticket.sprint_id = ""
                    ticket.updated_at = time.time()
            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, milestone_id: str | None = None
    ) -> dict[str, list[dict]]:
        """Kanban board view: columns by status."""
        tickets = self.list_tickets(sprint_id=sprint_id, milestone_id=milestone_id)
        columns: dict[str, list[dict]] = {s.value: [] for s in TicketStatus}
        for t in tickets:
            columns[t.status.value].append(self.ticket_overview(t))
        return columns

    # ── Deduplication ─────────────────────────────────────────

    def find_duplicates(self, dry_run: bool = True) -> dict[str, Any]:
        """Find and optionally remove duplicate tickets.

        Tickets are considered duplicates when they share the same
        normalised (task, project_id, type) tuple.  From each group the
        ticket with the most activity is kept (comments, tool assignment,
        latest update).
        """
        import re
        from collections import defaultdict

        def _normalize(text: str) -> str:
            return re.sub(r"\s+", " ", text.strip().lower())

        def _score(t: "Ticket") -> tuple:
            """Higher = better candidate to keep."""
            has_tool = 1 if t.assigned_tool else 0
            has_exec = 1 if t.last_execution else 0
            return (
                has_tool + has_exec,
                len(t.comments),
                t.updated_at,
                t.created_at,
            )

        groups: dict[tuple, list[Ticket]] = defaultdict(list)
        for ticket in self.tickets.values():
            key = (
                _normalize(ticket.task or ticket.title),
                ticket.project_id or "",
                ticket.type.value,
            )
            groups[key].append(ticket)

        dup_groups = {k: v for k, v in groups.items() if len(v) > 1}
        removed_ids: list[str] = []
        kept_ids: list[str] = []

        for key, tickets in dup_groups.items():
            ranked = sorted(tickets, key=_score, reverse=True)
            keeper = ranked[0]
            kept_ids.append(keeper.id)
            for dup in ranked[1:]:
                removed_ids.append(dup.id)
                if not dry_run:
                    self.tickets.pop(dup.id, None)

        if not dry_run and removed_ids:
            self._save()

        return {
            "duplicate_groups": len(dup_groups),
            "total_duplicates": len(removed_ids),
            "kept": kept_ids,
            "removed": removed_ids,
            "dry_run": dry_run,
        }

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

    def _save(self):
        if not self._persist_path:
            return
        if self._store is None:
            from proxym.storage import SQLiteNamespaceStore

            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()},
                "milestones": {mid: m.model_dump() for mid, m in self.milestones.items()},
            }
        )

    def _load(self):
        import json

        from proxym.storage import is_sqlite_database

        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 {}
                milestones_data = self._store.load_namespace("milestones") 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)
                for mid, mdata in milestones_data.items():
                    self.milestones[mid] = Milestone(**mdata)
                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)
                for mid, mdata in data.get("milestones", {}).items():
                    self.milestones[mid] = Milestone(**mdata)
                if self.tickets or self.sprints or self.milestones:
                    self._save()
        except Exception:
            pass
