import calendar
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

import typer
from rich import box
from rich.markdown import Markdown
from rich.panel import Panel
from rich.table import Table

from sunwaee.config import get_default_workspace, get_module_dir, is_machine_caller
from sunwaee.core.editor import open_editor
from sunwaee.core.fs import find_md, list_md, make_slug, read_md, search_md, write_md
from sunwaee.core.logger import get_logger
from sunwaee.core.output import console, error, success

from .model import Task, TaskPriority, TaskStatus

app = typer.Typer(name="task", help="Manage tasks.")
_log = get_logger("modules.tasks")

_PRIORITY_COLOR = {
    TaskPriority.HIGH: "red",
    TaskPriority.MEDIUM: "yellow",
    TaskPriority.LOW: "dim",
}

_PRIORITY_ORDER = {TaskPriority.HIGH: 0, TaskPriority.MEDIUM: 1, TaskPriority.LOW: 2}

_TASK_SORT_KEYS: dict = {
    "updated": (lambda t: t.updated_at, True),
    "created": (lambda t: t.created_at, True),
    "title": (lambda t: t.title.lower(), False),
    "due": (lambda t: t.due_date or "9999-99-99", False),
    "priority": (lambda t: _PRIORITY_ORDER[t.priority], False),
}

_STATUS_COLOR = {
    TaskStatus.TODO: "white",
    TaskStatus.IN_PROGRESS: "cyan",
    TaskStatus.DONE: "green",
}


def _resolve_dir(workspace: Optional[str]) -> tuple[str, Path]:
    name = workspace or get_default_workspace()
    return name, get_module_dir(name, "tasks")


def _load(path: Path) -> Task:
    meta, body = read_md(path)
    return Task.from_dict(meta, body)


def _save(task: Task, directory: Path, path: Optional[Path] = None) -> Path:
    if path is None:
        slug = make_slug(task.title, task.id)
        path = directory / f"{slug}.md"
    write_md(path, task.to_frontmatter_meta(), task.body)
    return path


def _enrich(task: Task, workspace: str, path: Path) -> dict:
    d = task.to_dict()
    d["workspace"] = workspace
    d["path"] = str(path)
    return d


def _sort_hierarchically(
    tasks: list[Task], paths: list[Path]
) -> tuple[list[Task], list[Path], list[int]]:
    """Reorder tasks so each parent immediately precedes its children (depth-first).
    Returns (tasks, paths, depths) where depth=0 is a root task."""
    by_id: dict[str, tuple[Task, Path]] = {t.id: (t, p) for t, p in zip(tasks, paths)}
    result_t: list[Task] = []
    result_p: list[Path] = []
    result_d: list[int] = []
    visited: set[str] = set()

    def _walk(task_id: str, depth: int) -> None:
        if task_id in visited or task_id not in by_id:  # pragma: no cover
            return
        visited.add(task_id)
        t, p = by_id[task_id]
        result_t.append(t)
        result_p.append(p)
        result_d.append(depth)
        for child_t, child_p in [
            (ct, cp) for ct, cp in zip(tasks, paths) if ct.parent_id == task_id
        ]:
            _walk(child_t.id, depth + 1)

    for t in tasks:
        if t.parent_id is None or t.parent_id not in by_id:
            _walk(t.id, 0)

    return result_t, result_p, result_d


def _find_children(parent_id: str, directory: Path) -> list[tuple[Task, Path]]:
    """Return all tasks whose parent_id matches the given id."""
    results = []
    for p in list_md(directory):
        task = _load(p)
        if task.parent_id == parent_id:
            results.append((task, p))
    return results


def _complete_cascade(
    task: Task, path: Path, directory: Path
) -> list[tuple[Task, Path]]:
    """Mark task and all descendants done. Returns list of all affected (task, path)."""
    from sunwaee.modules.tasks.model import _now
    affected = []
    task.status = TaskStatus.DONE
    task.completed_at = _now()
    task.touch()
    _save(task, directory, path)
    affected.append((task, path))
    for child, child_path in _find_children(task.id, directory):
        affected.extend(_complete_cascade(child, child_path, directory))
    return affected


def _delete_cascade(task: Task, path: Path, directory: Path) -> list[tuple[Task, Path]]:
    """Delete task and all descendants. Returns list of all deleted (task, path)."""
    deleted = []
    for child, child_path in _find_children(task.id, directory):
        deleted.extend(_delete_cascade(child, child_path, directory))
    path.unlink()
    deleted.append((task, path))
    return deleted


_DUE_HELP = "today, tomorrow, this-week, this-month, overdue, or YYYY-MM-DD"


def _resolve_due_date(expr: str) -> str:
    """Resolve a due date expression to a concrete YYYY-MM-DD string.

    Natural language expressions resolve to a single date:
      today       → today
      tomorrow    → tomorrow
      this-week   → end of the current week (Sunday)
      this-month  → last day of the current month
      YYYY-MM-DD  → validated and returned as-is

    'overdue' is not valid here (no single date to resolve to).
    """
    today = datetime.now().date()
    expr = expr.strip().lower().replace(" ", "-")

    if expr == "today":
        return today.isoformat()
    if expr == "tomorrow":
        return (today + timedelta(days=1)).isoformat()
    if expr == "this-week":
        end = today - timedelta(days=today.weekday()) + timedelta(days=6)
        return end.isoformat()
    if expr == "this-month":
        last_day = calendar.monthrange(today.year, today.month)[1]
        return today.replace(day=last_day).isoformat()
    if expr == "overdue":
        error(
            "'overdue' is not a valid due date for creating/editing a task. "
            f"Use {_DUE_HELP}",
            "VALIDATION_ERROR",
        )
    try:
        datetime.strptime(expr, "%Y-%m-%d")
        return expr
    except ValueError:
        error(
            f"Invalid due date: {expr!r}. Use {_DUE_HELP}",
            "VALIDATION_ERROR",
        )


def _parse_due(due_date: Optional[str]) -> Optional[str]:
    if due_date is None:
        return None
    return _resolve_due_date(due_date)


def _due_filter_range(expr: str) -> tuple[Optional[str], Optional[str]]:
    """Return (start, end) date strings (inclusive) for a due-filter expression."""
    today = datetime.now().date()
    expr = expr.strip().lower().replace(" ", "-")

    if expr == "this-week":
        start = today - timedelta(days=today.weekday())
        end = start + timedelta(days=6)
        return start.isoformat(), end.isoformat()
    if expr == "this-month":
        last_day = calendar.monthrange(today.year, today.month)[1]
        return today.replace(day=1).isoformat(), today.replace(day=last_day).isoformat()
    if expr == "overdue":
        return None, (today - timedelta(days=1)).isoformat()
    # today, tomorrow, exact date — delegate to _resolve_due_date for a single date
    d = _resolve_due_date(expr)
    return d, d


def _matches_due_filter(task_due: Optional[str], start: Optional[str], end: Optional[str]) -> bool:
    if task_due is None:
        return False
    if start is not None and task_due < start:
        return False
    if end is not None and task_due > end:
        return False
    return True


@app.command("create")
def create(
    title: str = typer.Option(..., "--title", "-t", help="Task title"),
    priority: TaskPriority = typer.Option(TaskPriority.MEDIUM, "--priority", "-p"),
    due: Optional[str] = typer.Option(None, "--due", help=f"Due date ({_DUE_HELP})"),
    tags: Optional[str] = typer.Option(None, "--tags", help="Comma-separated tags"),
    body: Optional[str] = typer.Option(
        None, "--body", "-b", help="Task description (markdown)"
    ),
    parent: Optional[str] = typer.Option(
        None, "--parent", help="Parent task id, slug, or title"
    ),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Create a new task."""
    ws, directory = _resolve_dir(workspace)
    tag_list = [t.strip() for t in tags.split(",")] if tags else []
    due_date = _parse_due(due)

    parent_id = None
    if parent:
        parent_path = find_md(parent, directory)
        if parent_path is None:
            error(f"Parent task not found: {parent!r}", "NOT_FOUND")
        parent_id = _load(parent_path).id

    if body is None and not is_machine_caller():  # pragma: no cover
        body = open_editor()

    task = Task(
        title=title,
        priority=priority,
        due_date=due_date,
        parent_id=parent_id,
        tags=tag_list,
        body=body or "",
    )
    path = _save(task, directory)
    _log.info(
        "Created task %r (id=%s, parent=%s) at %s", title, task.id, parent_id, path
    )

    success(
        _enrich(task, ws, path),
        human_fn=lambda: [
            console.print(
                f"[green]Created[/green] task '[bold]{task.title}[/bold]'"
                + (f" (subtask of {parent})" if parent else "")
            ),
            console.print(f"[dim]{path}[/dim]"),
        ],
    )


@app.command("list")
def list_tasks(
    status: Optional[TaskStatus] = typer.Option(None, "--status", "-s"),
    priority: Optional[TaskPriority] = typer.Option(None, "--priority", "-p"),
    due: Optional[str] = typer.Option(
        None,
        "--due",
        "-d",
        help="Filter by due date: today, tomorrow, this-week, this-month, overdue, or YYYY-MM-DD",
    ),
    tags: Optional[str] = typer.Option(
        None, "--tags", help="Filter by tag (comma-separated)"
    ),
    parent: Optional[str] = typer.Option(
        None, "--parent", help="List subtasks of a given task"
    ),
    show_completed: bool = typer.Option(
        False, "--show-completed", help="Include completed tasks (hidden by default)"
    ),
    sort: str = typer.Option(
        "updated", "--sort", help="Sort by: updated (default), created, title, due, priority"
    ),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """List tasks. Completed tasks are hidden by default."""
    ws, directory = _resolve_dir(workspace)
    if sort not in _TASK_SORT_KEYS:
        error(f"Invalid --sort value: {sort!r}. Choose from: {', '.join(_TASK_SORT_KEYS)}", "VALIDATION_ERROR")

    paths = list_md(directory)
    tasks = [_load(p) for p in paths]

    if parent:
        parent_path = find_md(parent, directory)
        if parent_path is None:
            error(f"Parent task not found: {parent!r}", "NOT_FOUND")
        parent_id = _load(parent_path).id
        pairs = [(t, p) for t, p in zip(tasks, paths) if t.parent_id == parent_id]
    else:
        pairs = list(zip(tasks, paths))

    if not show_completed:
        pairs = [(t, p) for t, p in pairs if t.status != TaskStatus.DONE]
    if status:
        pairs = [(t, p) for t, p in pairs if t.status == status]
    if priority:
        pairs = [(t, p) for t, p in pairs if t.priority == priority]
    if due:
        due_start, due_end = _due_filter_range(due)
        pairs = [(t, p) for t, p in pairs if _matches_due_filter(t.due_date, due_start, due_end)]
    if tags:
        filter_tags = {t.strip().lower() for t in tags.split(",")}
        pairs = [
            (t, p)
            for t, p in pairs
            if any(tag.lower() in filter_tags for tag in t.tags)
        ]

    key_fn, reverse = _TASK_SORT_KEYS[sort]
    pairs = sorted(pairs, key=lambda tp: key_fn(tp[0]), reverse=reverse)
    tasks, paths = (list(x) for x in zip(*pairs)) if pairs else ([], [])
    tasks, paths, depths = _sort_hierarchically(tasks, paths)

    def human_output() -> None:  # pragma: no cover
        if not tasks:
            console.print(f"[dim]No tasks in workspace '{ws}'.[/dim]")
            return
        table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
        table.add_column("ID", style="dim", width=10, no_wrap=True)
        table.add_column("Title")
        table.add_column("Status", width=12)
        table.add_column("Priority", width=10)
        table.add_column("Due", style="dim", width=12)
        if show_completed:
            table.add_column("Completed", style="dim", width=12)
        table.add_column("Tags")
        table.add_column("Path", style="dim")
        for task, path, depth in zip(tasks, paths, depths):
            s_color = _STATUS_COLOR[task.status]
            p_color = _PRIORITY_COLOR[task.priority]
            indent = "  " * depth
            row = [
                task.id[:8],
                f"{indent}{task.title}",
                f"[{s_color}]{task.status.value}[/{s_color}]",
                f"[{p_color}]{task.priority.value}[/{p_color}]",
                task.due_date or "—",
            ]
            if show_completed:
                row.append(task.completed_at[:10] if task.completed_at else "—")
            row += [
                ", ".join(task.tags) if task.tags else "—",
                str(path.resolve()),
            ]
            table.add_row(*row)
        console.print(table)

    success([_enrich(t, ws, p) for t, p in zip(tasks, paths)], human_fn=human_output)


@app.command("show")
def show(
    query: str = typer.Argument(..., help="Task id, slug, or title"),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Show a task and its subtasks."""
    ws, directory = _resolve_dir(workspace)
    path = find_md(query, directory)
    if path is None:
        error(f"Task not found: {query!r} (workspace: {ws!r})", "NOT_FOUND")

    task = _load(path)
    children = _find_children(task.id, directory)

    def human_output() -> None:  # pragma: no cover
        s_color = _STATUS_COLOR[task.status]
        p_color = _PRIORITY_COLOR[task.priority]
        completed_str = task.completed_at[:10] if task.completed_at else "—"
        meta_line = "\n".join([
            f"[dim]id:[/dim] {task.id[:8]}  "
            f"[dim]workspace:[/dim] {ws}  "
            f"[dim]status:[/dim] [{s_color}]{task.status.value}[/{s_color}]  "
            f"[dim]priority:[/dim] [{p_color}]{task.priority.value}[/{p_color}]  "
            f"[dim]due:[/dim] {task.due_date or '—'}  "
            f"[dim]completed:[/dim] {completed_str}  "
            f"[dim]tags:[/dim] {', '.join(task.tags) if task.tags else '—'}",
            f"[dim]path:[/dim] {path}",
        ])
        console.print(
            Panel(
                meta_line,
                title=f"[bold]{task.title}[/bold]",
                box=box.ROUNDED,
                expand=False,
            )
        )
        if task.body:
            console.print(Markdown(task.body))
        if children:
            console.print("")
            console.print("[dim]Subtasks:[/dim]")
            for child, _ in children:
                s_color = _STATUS_COLOR[child.status]
                console.print(
                    f"  ↳ [{s_color}]{child.status.value}[/{s_color}]  [bold]{child.title}[/bold]  [dim]{child.id[:8]}[/dim]"
                )

    data = _enrich(task, ws, path)
    data["subtasks"] = [_enrich(c, ws, cp) for c, cp in children]
    success(data, human_fn=human_output)


@app.command("complete")
def complete(
    query: str = typer.Argument(..., help="Task id, slug, or title"),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Mark a task and all its subtasks as done."""
    ws, directory = _resolve_dir(workspace)
    path = find_md(query, directory)
    if path is None:
        error(f"Task not found: {query!r} (workspace: {ws!r})", "NOT_FOUND")

    task = _load(path)
    affected = _complete_cascade(task, path, directory)
    _log.info(
        "Marked task %r (id=%s) complete; %d total affected", task.title, task.id, len(affected)
    )

    def human_output() -> None:  # pragma: no cover
        console.print(f"[green]Completed[/green] '[bold]{task.title}[/bold]'")
        console.print(f"[dim]{path}[/dim]")
        if len(affected) > 1:
            console.print(f"[dim]Also completed {len(affected) - 1} subtask(s).[/dim]")

    data = _enrich(task, ws, path)
    data["completed"] = [_enrich(t, ws, p) for t, p in affected]
    success(data, human_fn=human_output)


@app.command("uncomplete")
def uncomplete(
    query: str = typer.Argument(..., help="Task id, slug, or title"),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Reset a task (and its subtasks) back to todo, clearing the completion date."""
    ws, directory = _resolve_dir(workspace)
    path = find_md(query, directory)
    if path is None:
        error(f"Task not found: {query!r} (workspace: {ws!r})", "NOT_FOUND")

    task = _load(path)
    affected: list[tuple[Task, Path]] = []

    def _reset(t: Task, p: Path) -> None:
        t.status = TaskStatus.TODO
        t.completed_at = None
        t.touch()
        _save(t, directory, p)
        affected.append((t, p))
        for child, child_path in _find_children(t.id, directory):
            _reset(child, child_path)

    _reset(task, path)
    _log.info(
        "Uncompleted task %r (id=%s); %d total affected", task.title, task.id, len(affected)
    )

    def human_output() -> None:  # pragma: no cover
        console.print(f"[yellow]Uncompleted[/yellow] '[bold]{task.title}[/bold]'")
        console.print(f"[dim]{path}[/dim]")
        if len(affected) > 1:
            console.print(f"[dim]Also reset {len(affected) - 1} subtask(s).[/dim]")

    data = _enrich(task, ws, path)
    data["uncompleted"] = [_enrich(t, ws, p) for t, p in affected]
    success(data, human_fn=human_output)


@app.command("edit")
def edit(
    query: str = typer.Argument(..., help="Task id, slug, or title"),
    title: Optional[str] = typer.Option(None, "--title", "-t"),
    status: Optional[TaskStatus] = typer.Option(None, "--status", "-s"),
    priority: Optional[TaskPriority] = typer.Option(None, "--priority", "-p"),
    due: Optional[str] = typer.Option(None, "--due", help=f"New due date ({_DUE_HELP})"),
    clear_due: bool = typer.Option(False, "--clear-due", help="Remove the due date"),
    tags: Optional[str] = typer.Option(None, "--tags"),
    body: Optional[str] = typer.Option(None, "--body", "-b"),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Edit a task. Without flags (human caller), opens $EDITOR for the description."""
    ws, directory = _resolve_dir(workspace)
    path = find_md(query, directory)
    if path is None:
        error(f"Task not found: {query!r} (workspace: {ws!r})", "NOT_FOUND")

    task = _load(path)
    changed = False

    if title is not None:
        task.title = title
        changed = True
    if status is not None:
        task.status = status
        if status != TaskStatus.DONE:
            task.completed_at = None
        changed = True
    if priority is not None:
        task.priority = priority
        changed = True
    if clear_due:
        task.due_date = None
        changed = True
    elif due is not None:
        task.due_date = _parse_due(due)
        changed = True
    if tags is not None:
        task.tags = [t.strip() for t in tags.split(",")]
        changed = True
    if body is not None:
        task.body = body
        changed = True

    if not changed and not is_machine_caller():  # pragma: no cover
        task.body = open_editor(task.body)
        changed = True

    if changed:
        task.touch()
        _save(task, directory, path)
        _log.info("Updated task %r (id=%s) at %s", task.title, task.id, path)

    def _saved() -> None:  # pragma: no cover
        console.print(f"[green]Saved[/green] '[bold]{task.title}[/bold]'")
        console.print(f"[dim]{path}[/dim]")

    success(_enrich(task, ws, path), human_fn=_saved)


@app.command("delete")
def delete(
    query: str = typer.Argument(..., help="Task id, slug, or title"),
    confirm: bool = typer.Option(False, "--confirm", help="Confirm deletion"),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Delete a task and all its subtasks."""
    if not confirm:
        error("Pass --confirm to delete a task", "CONFIRMATION_REQUIRED")

    ws, directory = _resolve_dir(workspace)
    path = find_md(query, directory)
    if path is None:
        error(f"Task not found: {query!r} (workspace: {ws!r})", "NOT_FOUND")

    task = _load(path)
    deleted = _delete_cascade(task, path, directory)
    _log.info(
        "Deleted task %r (id=%s); %d total deleted", task.title, task.id, len(deleted)
    )

    def human_output() -> None:  # pragma: no cover
        console.print(f"[red]Deleted[/red] '[bold]{task.title}[/bold]'")
        console.print(f"[dim]{path}[/dim]")
        if len(deleted) > 1:
            console.print(f"[dim]Also deleted {len(deleted) - 1} subtask(s).[/dim]")

    success(
        {
            "deleted": [
                {"id": t.id, "title": t.title, "path": str(p)} for t, p in deleted
            ]
        },
        human_fn=human_output,
    )


@app.command("search")
def search(
    query: str = typer.Argument(..., help="Search term"),
    workspace: Optional[str] = typer.Option(
        None, "--workspace", "-w", help="Workspace name (default: from config)"
    ),
) -> None:
    """Search tasks by title, tags, or description."""
    ws, directory = _resolve_dir(workspace)
    paths = search_md(query, directory)
    tasks = [_load(p) for p in paths]

    def human_output() -> None:  # pragma: no cover
        if not tasks:
            console.print(
                f"[dim]No tasks matching '{query}' in workspace '{ws}'.[/dim]"
            )
            return
        table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
        table.add_column("ID", style="dim", width=10, no_wrap=True)
        table.add_column("Title")
        table.add_column("Status", width=12)
        table.add_column("Priority", width=10)
        table.add_column("Due", style="dim", width=12)
        table.add_column("Path", style="dim")
        for task, path in zip(tasks, paths):
            s_color = _STATUS_COLOR[task.status]
            p_color = _PRIORITY_COLOR[task.priority]
            table.add_row(
                task.id[:8],
                f"[dim](subtask)[/dim] {task.title}" if task.parent_id else task.title,
                f"[{s_color}]{task.status.value}[/{s_color}]",
                f"[{p_color}]{task.priority.value}[/{p_color}]",
                task.due_date or "—",
                str(path.resolve()),
            )
        console.print(table)

    success([_enrich(t, ws, p) for t, p in zip(tasks, paths)], human_fn=human_output)
