"""Main CLI entry point for Todo CLI."""

from datetime import datetime
from typing import Optional

import typer
from rich.console import Console

from . import __version__
from .database import Database
from .projects import ProjectManager
from .display import (
    display_todos,
    display_todo_detail,
    display_stats,
    display_projects,
    display_project_detail,
    success,
    error,
    warning,
    info,
)
from .models import Priority, Status
from .reports import daily_report, weekly_report, project_report
from .export import export_todos
from .config import (
    get_config, get_config_warnings, save_config, Config, DEFAULT_CONFIG_PATH,
    VALID_PRIORITIES, VALID_DATE_FORMATS, VALID_TIME_FORMATS, VALID_COLOR_SCHEMES,
)

app = typer.Typer(
    name="todo",
    help="A CLI todo manager with time tracking.",
    no_args_is_help=True,
)
console = Console()

# Project subcommands
project_app = typer.Typer(
    name="project",
    help="Manage projects.",
    no_args_is_help=True,
)
app.add_typer(project_app, name="project")


def get_db() -> Database:
    """Get database instance."""
    return Database()


def get_project_manager() -> ProjectManager:
    """Get project manager instance."""
    return ProjectManager(get_db().db_path)


def parse_priority(priority: str) -> Priority:
    """Parse priority string to Priority enum."""
    priority_map = {
        "p0": Priority.P0,
        "p1": Priority.P1,
        "p2": Priority.P2,
        "p3": Priority.P3,
        "0": Priority.P0,
        "1": Priority.P1,
        "2": Priority.P2,
        "3": Priority.P3,
    }
    # Use config default if no match
    config = get_config()
    default = priority_map.get(config.default_priority.lower(), Priority.P2)
    return priority_map.get(priority.lower(), default)


def parse_date(date_str: str) -> Optional[datetime]:
    """Parse date string to datetime."""
    if not date_str:
        return None

    formats = [
        "%Y-%m-%d",
        "%Y-%m-%d %H:%M",
        "%m/%d/%Y",
        "%m/%d",
    ]

    for fmt in formats:
        try:
            dt = datetime.strptime(date_str, fmt)
            # Handle year-less formats
            if dt.year == 1900:
                dt = dt.replace(year=datetime.now().year)
            return dt
        except ValueError:
            continue

    return None


@app.command()
def add(
    task: str = typer.Argument(..., help="The task description"),
    priority: Optional[str] = typer.Option(None, "-p", "--priority", help="Priority (p0-p3, default from config)"),
    project: Optional[str] = typer.Option(None, "-P", "--project", help="Project name"),
    tags: Optional[str] = typer.Option(None, "-t", "--tags", help="Comma-separated tags"),
    due: Optional[str] = typer.Option(None, "-d", "--due", help="Due date (YYYY-MM-DD)"),
):
    """Add a new todo."""
    db = get_db()

    # Use config default if no priority specified
    if priority is None:
        priority = get_config().default_priority
    parsed_priority = parse_priority(priority)
    parsed_due = parse_date(due) if due else None
    parsed_tags = [t.strip() for t in tags.split(",")] if tags else []

    # Resolve project name to project_id if provided
    project_id = None
    if project:
        pm = get_project_manager()
        project_obj = pm.get_project_by_name(project)
        if not project_obj:
            error(f"Project '{project}' not found")
            raise typer.Exit(1)
        project_id = project_obj.id

    todo = db.add(
        task=task,
        priority=parsed_priority,
        project_id=project_id,
        tags=parsed_tags,
        due_date=parsed_due,
    )

    success(f"Added todo #{todo.id}: {task}")

    # Auto-start timer if configured
    config = get_config()
    if config.auto_start_on_add:
        # Stop any existing timer first
        active = db.get_active_timer()
        if active and active.id != todo.id:
            db.stop_timer(active.id)
            warning(f"Stopped timer on todo #{active.id}")
        db.start_timer(todo.id)
        info(f"Timer started automatically")


@app.command("list")
def list_todos(
    all_: Optional[bool] = typer.Option(None, "-a", "--all", help="Include completed todos (default from config)"),
    project: Optional[str] = typer.Option(None, "-P", "--project", help="Filter by project name"),
    status: Optional[str] = typer.Option(None, "-s", "--status", help="Filter by status"),
):
    """List todos."""
    db = get_db()

    # Use config default if --all not specified
    config = get_config()
    include_done = all_ if all_ is not None else config.show_completed_in_list

    status_filter = None
    if status:
        status_map = {"todo": Status.TODO, "doing": Status.DOING, "done": Status.DONE}
        status_filter = status_map.get(status.lower())

    # Resolve project name to project_id if provided
    # If project exists in projects table, use project_id filter
    # Otherwise check if there are legacy tasks with that project string (backwards compatibility)
    project_id = None
    legacy_project = None
    if project:
        pm = get_project_manager()
        project_obj = pm.get_project_by_name(project)
        if project_obj:
            project_id = project_obj.id
        else:
            # Check if there are any legacy tasks with this project string
            legacy_tasks = db.list_all(project=project, include_done=True)
            if legacy_tasks:
                # Backwards compatibility: use legacy project string filter
                legacy_project = project
            else:
                error(f"Project '{project}' not found")
                raise typer.Exit(1)

    todos = db.list_all(status=status_filter, project_id=project_id, project=legacy_project, include_done=include_done)
    display_todos(todos)


@app.command()
def show(
    todo_id: int = typer.Argument(..., help="Todo ID"),
):
    """Show detailed view of a todo."""
    db = get_db()
    todo = db.get(todo_id)

    if not todo:
        error(f"Todo #{todo_id} not found")
        raise typer.Exit(1)

    display_todo_detail(todo)


@app.command()
def done(
    todo_id: int = typer.Argument(..., help="Todo ID to mark as done"),
):
    """Mark a todo as done."""
    db = get_db()
    todo = db.mark_done(todo_id)

    if not todo:
        error(f"Todo #{todo_id} not found")
        raise typer.Exit(1)

    time_str = todo.format_time()
    success(f"Completed todo #{todo_id}: {todo.task}")
    if todo.time_spent.total_seconds() > 0:
        info(f"Total time spent: {time_str}")


@app.command()
def delete(
    todo_id: int = typer.Argument(..., help="Todo ID to delete"),
    force: bool = typer.Option(False, "-f", "--force", help="Skip confirmation"),
):
    """Delete a todo."""
    db = get_db()
    todo = db.get(todo_id)

    if not todo:
        error(f"Todo #{todo_id} not found")
        raise typer.Exit(1)

    # Check config for confirm_delete setting
    config = get_config()
    should_confirm = config.confirm_delete and not force

    if should_confirm:
        confirm = typer.confirm(f"Delete todo #{todo_id}: '{todo.task}'?")
        if not confirm:
            info("Cancelled")
            return

    db.delete(todo_id)
    success(f"Deleted todo #{todo_id}")


@app.command()
def start(
    todo_id: int = typer.Argument(..., help="Todo ID to start tracking"),
):
    """Start time tracking on a todo."""
    db = get_db()

    # Check for existing active timer
    active = db.get_active_timer()
    if active and active.id != todo_id:
        warning(f"Stopping timer on todo #{active.id}: {active.task}")
        db.stop_timer(active.id)

    todo = db.start_timer(todo_id)

    if not todo:
        error(f"Todo #{todo_id} not found")
        raise typer.Exit(1)

    success(f"Started tracking todo #{todo_id}: {todo.task}")


@app.command()
def stop(
    todo_id: Optional[int] = typer.Argument(None, help="Todo ID (optional, stops any active)"),
):
    """Stop time tracking."""
    db = get_db()

    todo = db.stop_timer(todo_id)

    if not todo:
        if todo_id:
            error(f"No active timer on todo #{todo_id}")
        else:
            info("No active timer")
        return

    time_str = todo.format_time()
    success(f"Stopped tracking todo #{todo.id}: {todo.task}")
    info(f"Time spent: {time_str}")


@app.command()
def status(
    todo_id: int = typer.Argument(..., help="Todo ID"),
    new_status: str = typer.Argument(..., help="New status (todo, doing, done)"),
):
    """Change todo status."""
    db = get_db()
    todo = db.get(todo_id)

    if not todo:
        error(f"Todo #{todo_id} not found")
        raise typer.Exit(1)

    status_map = {"todo": Status.TODO, "doing": Status.DOING, "done": Status.DONE}
    parsed_status = status_map.get(new_status.lower())

    if not parsed_status:
        error(f"Invalid status: {new_status}. Use: todo, doing, done")
        raise typer.Exit(1)

    todo.status = parsed_status
    if parsed_status == Status.DONE:
        todo.completed_at = datetime.now()

    db.update(todo)
    success(f"Updated todo #{todo_id} status to {parsed_status.value}")


@app.command()
def edit(
    todo_id: int = typer.Argument(..., help="Todo ID"),
    task: Optional[str] = typer.Option(None, "-t", "--task", help="New task description"),
    priority: Optional[str] = typer.Option(None, "-p", "--priority", help="New priority"),
    project: Optional[str] = typer.Option(None, "-P", "--project", help="New project"),
    tags: Optional[str] = typer.Option(None, "--tags", help="New tags (comma-separated)"),
    due: Optional[str] = typer.Option(None, "-d", "--due", help="New due date"),
):
    """Edit a todo."""
    db = get_db()
    todo = db.get(todo_id)

    if not todo:
        error(f"Todo #{todo_id} not found")
        raise typer.Exit(1)

    if task:
        todo.task = task
    if priority:
        todo.priority = parse_priority(priority)
    if project is not None:
        todo.project = project if project else None
    if tags is not None:
        todo.tags = [t.strip() for t in tags.split(",")] if tags else []
    if due is not None:
        todo.due_date = parse_date(due)

    db.update(todo)
    success(f"Updated todo #{todo_id}")


# Project commands moved to project_app sub-application (see below)


@app.command()
def stats():
    """Show todo statistics."""
    db = get_db()
    stats_data = db.get_stats()
    display_stats(stats_data)


@app.command()
def active():
    """Show currently tracking todo."""
    db = get_db()
    todo = db.get_active_timer()

    if not todo:
        info("No active timer")
        return

    display_todo_detail(todo)


@app.command()
def version():
    """Show version."""
    console.print(f"Todo CLI v{__version__}")


@app.command()
def report(
    report_type: str = typer.Argument("daily", help="Report type: daily, weekly, project"),
    project: Optional[str] = typer.Option(None, "-P", "--project", help="Project for project report"),
):
    """Generate time tracking reports."""
    db = get_db()

    if report_type == "daily":
        daily_report(db)
    elif report_type == "weekly":
        weekly_report(db)
    elif report_type == "project":
        project_report(db, project)
    else:
        error(f"Unknown report type: {report_type}. Use: daily, weekly, project")
        raise typer.Exit(1)


@app.command()
def export(
    format: str = typer.Argument("json", help="Export format: json, csv, md"),
    output: Optional[str] = typer.Option(None, "-o", "--output", help="Output file path"),
    all_: bool = typer.Option(True, "-a", "--all", help="Include completed todos"),
    project: Optional[str] = typer.Option(None, "-P", "--project", help="Filter by project"),
):
    """Export todos to file."""
    from pathlib import Path

    db = get_db()
    output_path = Path(output) if output else None

    try:
        result_path = export_todos(db, format, output_path, include_done=all_, project=project)
        success(f"Exported to: {result_path}")
    except ValueError as e:
        error(str(e))
        raise typer.Exit(1)


@app.command()
def interactive():
    """Launch interactive menu mode."""
    from .interactive import run_interactive
    run_interactive()


@app.command("config")
def config_cmd(
    show: bool = typer.Option(False, "--show", "-s", help="Show current config"),
    init: bool = typer.Option(False, "--init", help="Create default config file"),
    set_option: Optional[str] = typer.Option(None, "--set", help="Set option (key=value)"),
    path: bool = typer.Option(False, "--path", help="Show config file path"),
    validate: bool = typer.Option(False, "--validate", "-v", help="Validate current config"),
):
    """View or modify configuration."""
    config = get_config()

    if path:
        console.print(f"Config file: {DEFAULT_CONFIG_PATH}")
        console.print(f"Exists: {DEFAULT_CONFIG_PATH.exists()}")
        return

    if validate:
        warnings = config.validate()
        if warnings:
            for warn in warnings:
                warning(warn)
        else:
            success("Config is valid")
        return

    if init:
        if DEFAULT_CONFIG_PATH.exists():
            if not typer.confirm("Config file exists. Overwrite?"):
                info("Cancelled")
                return
        config = Config()  # Reset to defaults
        save_config(config)
        success(f"Created config at: {DEFAULT_CONFIG_PATH}")
        return

    if set_option:
        try:
            key, value = set_option.split("=", 1)
            key = key.strip()
            value = value.strip()

            # Type conversion for known boolean fields
            if key in ("confirm_delete", "auto_start_on_add", "show_completed_in_list"):
                value = value.lower() in ("true", "1", "yes", "on")

            if not hasattr(config, key):
                error(f"Unknown config option: {key}")
                raise typer.Exit(1)

            # Validate specific fields before setting
            if key == "default_priority" and value.lower() not in VALID_PRIORITIES:
                error(f"Invalid priority. Valid: {', '.join(sorted(VALID_PRIORITIES))}")
                raise typer.Exit(1)

            if key == "date_format" and value not in VALID_DATE_FORMATS:
                error(f"Invalid date format. Valid: {', '.join(sorted(VALID_DATE_FORMATS))}")
                raise typer.Exit(1)

            if key == "time_format" and value not in VALID_TIME_FORMATS:
                error(f"Invalid time format. Valid: {', '.join(sorted(VALID_TIME_FORMATS))}")
                raise typer.Exit(1)

            if key == "color_scheme" and value.lower() not in VALID_COLOR_SCHEMES:
                error(f"Invalid color scheme. Valid: {', '.join(sorted(VALID_COLOR_SCHEMES))}")
                raise typer.Exit(1)

            setattr(config, key, value)
            save_config(config)
            success(f"Set {key} = {value}")
        except ValueError:
            error("Invalid format. Use: --set key=value")
            raise typer.Exit(1)
        return

    # Default: show config
    from rich.table import Table

    table = Table(title="Configuration", show_header=True)
    table.add_column("Option", style="cyan")
    table.add_column("Value", style="green")
    table.add_column("Description", style="dim")

    descriptions = {
        "default_priority": "Default priority for new todos",
        "date_format": "Date display format",
        "time_format": "Time display format (12h/24h)",
        "color_scheme": "Color scheme (auto/dark/light/none)",
        "confirm_delete": "Confirm before deleting",
        "auto_start_on_add": "Auto-start timer on add",
        "show_completed_in_list": "Show completed in list by default",
        "db_path": "Custom database path",
    }

    for key, desc in descriptions.items():
        value = getattr(config, key, None)
        value_str = str(value) if value is not None else "(default)"
        table.add_row(key, value_str, desc)

    console.print(table)
    console.print(f"\n[dim]Config file: {DEFAULT_CONFIG_PATH}[/dim]")


# ============================================================================
# PROJECT SUBCOMMANDS
# ============================================================================

@project_app.command("create")
def project_create(
    name: str = typer.Argument(..., help="Project name"),
    description: Optional[str] = typer.Option(None, "--description", "-d", help="Project description"),
    color: Optional[str] = typer.Option(None, "--color", "-c", help="Project color"),
):
    """Create a new project."""
    pm = get_project_manager()

    try:
        project = pm.create_project(name, description, color)
        success(f"Created project: {project.name}")
    except ValueError as e:
        error(str(e))
        raise typer.Exit(1)


@project_app.command("list")
def project_list(
    archived: bool = typer.Option(False, "--archived", "-a", help="Show archived projects"),
):
    """List all projects."""
    pm = get_project_manager()
    projects = pm.list_projects(archived=archived)

    if not projects:
        info("No projects found")
        return

    display_projects(projects)


@project_app.command("show")
def project_show(
    name: str = typer.Argument(..., help="Project name"),
):
    """Show project details."""
    pm = get_project_manager()
    project = pm.get_project_by_name(name)

    if not project:
        error(f"Project '{name}' not found")
        raise typer.Exit(1)

    # Get project stats and tasks
    stats = pm.get_project_stats(project.id)
    display_project_detail(stats)


@project_app.command("delete")
def project_delete(
    name: str = typer.Argument(..., help="Project name"),
    force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
):
    """Delete a project. Tasks in the project will persist."""
    pm = get_project_manager()
    project = pm.get_project_by_name(name)

    if not project:
        error(f"Project '{name}' not found")
        raise typer.Exit(1)

    # Confirm deletion
    if not force:
        confirm = typer.confirm(
            f"Delete project '{project.name}'? (Tasks will remain with no project)"
        )
        if not confirm:
            info("Cancelled")
            return

    if pm.delete_project(project.id):
        success(f"Deleted project: {project.name}")
    else:
        error("Failed to delete project")
        raise typer.Exit(1)


@project_app.command("update")
def project_update(
    name: str = typer.Argument(..., help="Current project name"),
    new_name: Optional[str] = typer.Option(None, "--name", "-n", help="New project name"),
    description: Optional[str] = typer.Option(None, "--description", "-d", help="New description"),
    color: Optional[str] = typer.Option(None, "--color", "-c", help="New color"),
):
    """Update project details."""
    pm = get_project_manager()
    project = pm.get_project_by_name(name)

    if not project:
        error(f"Project '{name}' not found")
        raise typer.Exit(1)

    try:
        updated = pm.update_project(project.id, new_name, description, color)
        if updated:
            success(f"Updated project: {updated.name}")
        else:
            error("Failed to update project")
            raise typer.Exit(1)
    except ValueError as e:
        error(str(e))
        raise typer.Exit(1)


@project_app.command("archive")
def project_archive(
    name: str = typer.Argument(..., help="Project name"),
):
    """Archive a project."""
    pm = get_project_manager()
    project = pm.get_project_by_name(name)

    if not project:
        error(f"Project '{name}' not found")
        raise typer.Exit(1)

    archived = pm.archive_project(project.id)
    if archived:
        success(f"Archived project: {archived.name}")
    else:
        error("Failed to archive project")
        raise typer.Exit(1)


@project_app.command("unarchive")
def project_unarchive(
    name: str = typer.Argument(..., help="Project name"),
):
    """Unarchive a project."""
    pm = get_project_manager()
    project = pm.get_project_by_name(name)

    if not project:
        error(f"Project '{name}' not found")
        raise typer.Exit(1)

    unarchived = pm.unarchive_project(project.id)
    if unarchived:
        success(f"Unarchived project: {unarchived.name}")
    else:
        error("Failed to unarchive project")
        raise typer.Exit(1)


# ============================================================================
# MAIN CALLBACK
# ============================================================================

@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
    """Todo CLI - A command-line todo manager with time tracking."""
    # Load config and display any warnings
    get_config()
    for warn in get_config_warnings():
        warning(warn)


if __name__ == "__main__":
    app()
