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 Note

app = typer.Typer(name="note", help="Manage notes.")
_log = get_logger("modules.notes")


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


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


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


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


@app.command("create")
def create(
    title: str = typer.Option(..., "--title", "-t", help="Note title"),
    tags: Optional[str] = typer.Option(None, "--tags", help="Comma-separated tags"),
    body: Optional[str] = typer.Option(None, "--body", "-b", help="Note body (markdown)"),
    workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace name (default: from config)"),
) -> None:
    """Create a new note."""
    ws, directory = _resolve_dir(workspace)
    tag_list = [t.strip() for t in tags.split(",")] if tags else []

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

    note = Note(title=title, tags=tag_list, body=body or "")
    path = _save(note, directory)
    _log.info("Created note %r (id=%s) at %s", title, note.id, path)

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

    success(_enrich(note, ws, path), human_fn=_created)


_NOTE_SORT_KEYS = {
    "updated": lambda n: n.updated_at,
    "created": lambda n: n.created_at,
    "title": lambda n: n.title.lower(),
}


@app.command("list")
def list_notes(
    tags: Optional[str] = typer.Option(None, "--tags", help="Filter by tag (comma-separated)"),
    sort: str = typer.Option(
        "updated", "--sort", help="Sort by: updated (default), created, title"
    ),
    workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace name (default: from config)"),
) -> None:
    """List all notes."""
    ws, directory = _resolve_dir(workspace)
    if sort not in _NOTE_SORT_KEYS:
        error(f"Invalid --sort value: {sort!r}. Choose from: {', '.join(_NOTE_SORT_KEYS)}", "VALIDATION_ERROR")

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

    if tags:
        filter_tags = {t.strip().lower() for t in tags.split(",")}
        pairs = [(n, p) for n, p in zip(notes, paths) if any(t.lower() in filter_tags for t in n.tags)]
        notes, paths = (list(x) for x in zip(*pairs)) if pairs else ([], [])

    key_fn = _NOTE_SORT_KEYS[sort]
    reverse = sort != "title"
    pairs = sorted(zip(notes, paths), key=lambda np: key_fn(np[0]), reverse=reverse)
    notes, paths = (list(x) for x in zip(*pairs)) if pairs else ([], [])

    def human_output() -> None:  # pragma: no cover
        if not notes:
            console.print(f"[dim]No notes 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("Tags")
        table.add_column("Updated", style="dim", width=12)
        table.add_column("Path", style="dim")
        for note, path in zip(notes, paths):
            table.add_row(
                note.id[:8],
                note.title,
                ", ".join(note.tags) if note.tags else "—",
                note.updated_at[:10],
                str(path.resolve()),
            )
        console.print(table)

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


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

    note = _load(path)
    _log.debug("Showing note %r (id=%s)", note.title, note.id)

    def human_output() -> None:  # pragma: no cover
        meta_line = "\n".join([
            f"[dim]id:[/dim] {note.id[:8]}  "
            f"[dim]workspace:[/dim] {ws}  "
            f"[dim]tags:[/dim] {', '.join(note.tags) if note.tags else '—'}  "
            f"[dim]updated:[/dim] {note.updated_at[:10]}",
            f"[dim]path:[/dim] {path}",
        ])
        console.print(
            Panel(meta_line, title=f"[bold]{note.title}[/bold]", box=box.ROUNDED, expand=False)
        )
        if note.body:
            console.print(Markdown(note.body))

    success(_enrich(note, ws, path), human_fn=human_output)


@app.command("edit")
def edit(
    query: str = typer.Argument(..., help="Note id, slug, or title"),
    title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
    tags: Optional[str] = typer.Option(None, "--tags", help="New tags (comma-separated)"),
    body: Optional[str] = typer.Option(None, "--body", "-b", help="New body"),
    workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace name (default: from config)"),
) -> None:
    """Edit a note. Without flags (human caller), opens $EDITOR for the body."""
    ws, directory = _resolve_dir(workspace)
    path = find_md(query, directory)
    if path is None:
        error(f"Note not found: {query!r} (workspace: {ws!r})", "NOT_FOUND")

    note = _load(path)
    changed = False

    if title is not None:
        note.title = title
        changed = True
    if tags is not None:
        note.tags = [t.strip() for t in tags.split(",")]
        changed = True
    if body is not None:
        note.body = body
        changed = True

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

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

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

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


@app.command("delete")
def delete(
    query: str = typer.Argument(..., help="Note 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 note."""
    if not confirm:
        error("Pass --confirm to delete a note", "CONFIRMATION_REQUIRED")

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

    note = _load(path)
    deleted_path = path
    path.unlink()
    _log.info("Deleted note %r (id=%s) from %s", note.title, note.id, deleted_path)

    def _deleted() -> None:  # pragma: no cover
        console.print(f"[red]Deleted[/red] '[bold]{note.title}[/bold]'")
        console.print(f"[dim]{deleted_path}[/dim]")

    success(
        {"id": note.id, "title": note.title, "workspace": ws, "path": str(deleted_path)},
        human_fn=_deleted,
    )


@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 notes by title, tags, or body content."""
    ws, directory = _resolve_dir(workspace)
    paths = search_md(query, directory)
    notes = [_load(p) for p in paths]

    def human_output() -> None:  # pragma: no cover
        if not notes:
            console.print(f"[dim]No notes 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("Tags")
        table.add_column("Updated", style="dim", width=12)
        table.add_column("Path", style="dim")
        for note, path in zip(notes, paths):
            table.add_row(
                note.id[:8],
                note.title,
                ", ".join(note.tags) if note.tags else "—",
                note.updated_at[:10],
                str(path.resolve()),
            )
        console.print(table)

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