from pathlib import Path
from typing import Optional
import json

import typer

from password_manager.manager import PasswordManager
from password_manager.entry_types import EntryType
import uvicorn
from . import api as api_module

import importlib

app = typer.Typer(
    help="SeedPass command line interface",
    invoke_without_command=True,
)

# Global option shared across all commands
fingerprint_option = typer.Option(
    None,
    "--fingerprint",
    "-f",
    help="Specify which seed profile to use",
)

# Sub command groups
entry_app = typer.Typer(help="Manage individual entries")
vault_app = typer.Typer(help="Manage the entire vault")
nostr_app = typer.Typer(help="Interact with Nostr relays")
config_app = typer.Typer(help="Get or set configuration values")
fingerprint_app = typer.Typer(help="Manage seed profiles")
util_app = typer.Typer(help="Utility commands")
api_app = typer.Typer(help="Run the API server")

app.add_typer(entry_app, name="entry")
app.add_typer(vault_app, name="vault")
app.add_typer(nostr_app, name="nostr")
app.add_typer(config_app, name="config")
app.add_typer(fingerprint_app, name="fingerprint")
app.add_typer(util_app, name="util")
app.add_typer(api_app, name="api")


def _get_pm(ctx: typer.Context) -> PasswordManager:
    """Return a PasswordManager optionally selecting a fingerprint."""
    fp = ctx.obj.get("fingerprint")
    if fp is None:
        pm = PasswordManager()
    else:
        pm = PasswordManager(fingerprint=fp)
    return pm


@app.callback(invoke_without_command=True)
def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) -> None:
    """SeedPass CLI entry point.

    When called without a subcommand this launches the interactive TUI.
    """
    ctx.obj = {"fingerprint": fingerprint}
    if ctx.invoked_subcommand is None:
        tui = importlib.import_module("main")
        raise typer.Exit(tui.main(fingerprint=fingerprint))


@entry_app.command("list")
def entry_list(
    ctx: typer.Context,
    sort: str = typer.Option(
        "index", "--sort", help="Sort by 'index', 'label', or 'username'"
    ),
    kind: Optional[str] = typer.Option(None, "--kind", help="Filter by entry type"),
    archived: bool = typer.Option(False, "--archived", help="Include archived"),
) -> None:
    """List entries in the vault."""
    pm = _get_pm(ctx)
    entries = pm.entry_manager.list_entries(
        sort_by=sort, filter_kind=kind, include_archived=archived
    )
    for idx, label, username, url, is_archived in entries:
        line = f"{idx}: {label}"
        if username:
            line += f" ({username})"
        if url:
            line += f" {url}"
        if is_archived:
            line += " [archived]"
        typer.echo(line)


@entry_app.command("search")
def entry_search(ctx: typer.Context, query: str) -> None:
    """Search entries."""
    pm = _get_pm(ctx)
    results = pm.entry_manager.search_entries(query)
    if not results:
        typer.echo("No matching entries found")
        return
    for idx, label, username, url, _arch in results:
        line = f"{idx}: {label}"
        if username:
            line += f" ({username})"
        if url:
            line += f" {url}"
        typer.echo(line)


@entry_app.command("get")
def entry_get(ctx: typer.Context, query: str) -> None:
    """Retrieve a single entry's secret."""
    pm = _get_pm(ctx)
    matches = pm.entry_manager.search_entries(query)
    if len(matches) == 0:
        typer.echo("No matching entries found")
        raise typer.Exit(code=1)
    if len(matches) > 1:
        typer.echo("Matches:")
        for idx, label, username, _url, _arch in matches:
            name = f"{idx}: {label}"
            if username:
                name += f" ({username})"
            typer.echo(name)
        raise typer.Exit(code=1)

    index = matches[0][0]
    entry = pm.entry_manager.retrieve_entry(index)
    etype = entry.get("type", entry.get("kind"))
    if etype == EntryType.PASSWORD.value:
        length = int(entry.get("length", 12))
        password = pm.password_generator.generate_password(length, index)
        typer.echo(password)
    elif etype == EntryType.TOTP.value:
        code = pm.entry_manager.get_totp_code(index, pm.parent_seed)
        typer.echo(code)
    else:
        typer.echo("Unsupported entry type")
        raise typer.Exit(code=1)


@entry_app.command("add")
def entry_add(
    ctx: typer.Context,
    label: str,
    length: int = typer.Option(12, "--length"),
    username: Optional[str] = typer.Option(None, "--username"),
    url: Optional[str] = typer.Option(None, "--url"),
) -> None:
    """Add a new password entry and output its index."""
    pm = _get_pm(ctx)
    index = pm.entry_manager.add_entry(label, length, username, url)
    typer.echo(str(index))
    pm.sync_vault()


@entry_app.command("add-totp")
def entry_add_totp(
    ctx: typer.Context,
    label: str,
    index: Optional[int] = typer.Option(None, "--index", help="Derivation index"),
    secret: Optional[str] = typer.Option(None, "--secret", help="Import secret"),
    period: int = typer.Option(30, "--period", help="TOTP period in seconds"),
    digits: int = typer.Option(6, "--digits", help="Number of TOTP digits"),
) -> None:
    """Add a TOTP entry and output the otpauth URI."""
    pm = _get_pm(ctx)
    uri = pm.entry_manager.add_totp(
        label,
        pm.parent_seed,
        index=index,
        secret=secret,
        period=period,
        digits=digits,
    )
    typer.echo(uri)
    pm.sync_vault()


@entry_app.command("add-ssh")
def entry_add_ssh(
    ctx: typer.Context,
    label: str,
    index: Optional[int] = typer.Option(None, "--index", help="Derivation index"),
    notes: str = typer.Option("", "--notes", help="Entry notes"),
) -> None:
    """Add an SSH key entry and output its index."""
    pm = _get_pm(ctx)
    idx = pm.entry_manager.add_ssh_key(
        label,
        pm.parent_seed,
        index=index,
        notes=notes,
    )
    typer.echo(str(idx))
    pm.sync_vault()


@entry_app.command("add-pgp")
def entry_add_pgp(
    ctx: typer.Context,
    label: str,
    index: Optional[int] = typer.Option(None, "--index", help="Derivation index"),
    key_type: str = typer.Option("ed25519", "--key-type", help="Key type"),
    user_id: str = typer.Option("", "--user-id", help="User ID"),
    notes: str = typer.Option("", "--notes", help="Entry notes"),
) -> None:
    """Add a PGP key entry and output its index."""
    pm = _get_pm(ctx)
    idx = pm.entry_manager.add_pgp_key(
        label,
        pm.parent_seed,
        index=index,
        key_type=key_type,
        user_id=user_id,
        notes=notes,
    )
    typer.echo(str(idx))
    pm.sync_vault()


@entry_app.command("add-nostr")
def entry_add_nostr(
    ctx: typer.Context,
    label: str,
    index: Optional[int] = typer.Option(None, "--index", help="Derivation index"),
    notes: str = typer.Option("", "--notes", help="Entry notes"),
) -> None:
    """Add a Nostr key entry and output its index."""
    pm = _get_pm(ctx)
    idx = pm.entry_manager.add_nostr_key(
        label,
        index=index,
        notes=notes,
    )
    typer.echo(str(idx))
    pm.sync_vault()


@entry_app.command("add-seed")
def entry_add_seed(
    ctx: typer.Context,
    label: str,
    index: Optional[int] = typer.Option(None, "--index", help="Derivation index"),
    words: int = typer.Option(24, "--words", help="Word count"),
    notes: str = typer.Option("", "--notes", help="Entry notes"),
) -> None:
    """Add a derived seed phrase entry and output its index."""
    pm = _get_pm(ctx)
    idx = pm.entry_manager.add_seed(
        label,
        pm.parent_seed,
        index=index,
        words_num=words,
        notes=notes,
    )
    typer.echo(str(idx))
    pm.sync_vault()


@entry_app.command("add-key-value")
def entry_add_key_value(
    ctx: typer.Context,
    label: str,
    value: str = typer.Option(..., "--value", help="Stored value"),
    notes: str = typer.Option("", "--notes", help="Entry notes"),
) -> None:
    """Add a key/value entry and output its index."""
    pm = _get_pm(ctx)
    idx = pm.entry_manager.add_key_value(label, value, notes=notes)
    typer.echo(str(idx))
    pm.sync_vault()


@entry_app.command("add-managed-account")
def entry_add_managed_account(
    ctx: typer.Context,
    label: str,
    index: Optional[int] = typer.Option(None, "--index", help="Derivation index"),
    notes: str = typer.Option("", "--notes", help="Entry notes"),
) -> None:
    """Add a managed account seed entry and output its index."""
    pm = _get_pm(ctx)
    idx = pm.entry_manager.add_managed_account(
        label,
        pm.parent_seed,
        index=index,
        notes=notes,
    )
    typer.echo(str(idx))
    pm.sync_vault()


@entry_app.command("modify")
def entry_modify(
    ctx: typer.Context,
    entry_id: int,
    label: Optional[str] = typer.Option(None, "--label"),
    username: Optional[str] = typer.Option(None, "--username"),
    url: Optional[str] = typer.Option(None, "--url"),
    notes: Optional[str] = typer.Option(None, "--notes"),
    period: Optional[int] = typer.Option(
        None, "--period", help="TOTP period in seconds"
    ),
    digits: Optional[int] = typer.Option(None, "--digits", help="TOTP digits"),
    value: Optional[str] = typer.Option(None, "--value", help="New value"),
) -> None:
    """Modify an existing entry."""
    pm = _get_pm(ctx)
    try:
        pm.entry_manager.modify_entry(
            entry_id,
            username=username,
            url=url,
            notes=notes,
            label=label,
            period=period,
            digits=digits,
            value=value,
        )
    except ValueError as e:
        typer.echo(str(e))
        raise typer.Exit(code=1)
    pm.sync_vault()


@entry_app.command("archive")
def entry_archive(ctx: typer.Context, entry_id: int) -> None:
    """Archive an entry."""
    pm = _get_pm(ctx)
    pm.entry_manager.archive_entry(entry_id)
    typer.echo(str(entry_id))
    pm.sync_vault()


@entry_app.command("unarchive")
def entry_unarchive(ctx: typer.Context, entry_id: int) -> None:
    """Restore an archived entry."""
    pm = _get_pm(ctx)
    pm.entry_manager.restore_entry(entry_id)
    typer.echo(str(entry_id))
    pm.sync_vault()


@entry_app.command("totp-codes")
def entry_totp_codes(ctx: typer.Context) -> None:
    """Display all current TOTP codes."""
    pm = _get_pm(ctx)
    pm.handle_display_totp_codes()


@entry_app.command("export-totp")
def entry_export_totp(
    ctx: typer.Context, file: str = typer.Option(..., help="Output file")
) -> None:
    """Export all TOTP secrets to a JSON file."""
    pm = _get_pm(ctx)
    data = pm.entry_manager.export_totp_entries(pm.parent_seed)
    Path(file).write_text(json.dumps(data, indent=2))
    typer.echo(str(file))


@vault_app.command("export")
def vault_export(
    ctx: typer.Context, file: str = typer.Option(..., help="Output file")
) -> None:
    """Export the vault."""
    pm = _get_pm(ctx)
    pm.handle_export_database(Path(file))
    typer.echo(str(file))


@vault_app.command("import")
def vault_import(
    ctx: typer.Context, file: str = typer.Option(..., help="Input file")
) -> None:
    """Import a vault from an encrypted JSON file."""
    pm = _get_pm(ctx)
    pm.handle_import_database(Path(file))
    pm.sync_vault()
    typer.echo(str(file))


@vault_app.command("change-password")
def vault_change_password(ctx: typer.Context) -> None:
    """Change the master password used for encryption."""
    pm = _get_pm(ctx)
    pm.change_password()


@vault_app.command("lock")
def vault_lock(ctx: typer.Context) -> None:
    """Lock the vault and clear sensitive data from memory."""
    pm = _get_pm(ctx)
    pm.lock_vault()
    typer.echo("locked")


@vault_app.command("stats")
def vault_stats(ctx: typer.Context) -> None:
    """Display statistics about the current seed profile."""
    pm = _get_pm(ctx)
    stats = pm.get_profile_stats()
    typer.echo(json.dumps(stats, indent=2))


@vault_app.command("reveal-parent-seed")
def vault_reveal_parent_seed(
    ctx: typer.Context,
    file: Optional[str] = typer.Option(
        None, "--file", help="Save encrypted seed to this path"
    ),
) -> None:
    """Display the parent seed and optionally write an encrypted backup file."""
    pm = _get_pm(ctx)
    pm.handle_backup_reveal_parent_seed(Path(file) if file else None)


@nostr_app.command("sync")
def nostr_sync(ctx: typer.Context) -> None:
    """Sync with configured Nostr relays."""
    pm = _get_pm(ctx)
    result = pm.sync_vault()
    if result:
        typer.echo("Event IDs:")
        typer.echo(f"- manifest: {result['manifest_id']}")
        for cid in result["chunk_ids"]:
            typer.echo(f"- chunk: {cid}")
        for did in result["delta_ids"]:
            typer.echo(f"- delta: {did}")
    else:
        typer.echo("Error: Failed to sync vault")


@nostr_app.command("get-pubkey")
def nostr_get_pubkey(ctx: typer.Context) -> None:
    """Display the active profile's npub."""
    pm = _get_pm(ctx)
    npub = pm.nostr_client.key_manager.get_npub()
    typer.echo(npub)


@config_app.command("get")
def config_get(ctx: typer.Context, key: str) -> None:
    """Get a configuration value."""
    pm = _get_pm(ctx)
    value = pm.config_manager.load_config(require_pin=False).get(key)
    if value is None:
        typer.echo("Key not found")
    else:
        typer.echo(str(value))


@config_app.command("set")
def config_set(ctx: typer.Context, key: str, value: str) -> None:
    """Set a configuration value."""
    pm = _get_pm(ctx)
    cfg = pm.config_manager

    mapping = {
        "inactivity_timeout": lambda v: cfg.set_inactivity_timeout(float(v)),
        "secret_mode_enabled": lambda v: cfg.set_secret_mode_enabled(
            v.lower() in ("1", "true", "yes", "y", "on")
        ),
        "clipboard_clear_delay": lambda v: cfg.set_clipboard_clear_delay(int(v)),
        "additional_backup_path": lambda v: cfg.set_additional_backup_path(v or None),
        "relays": lambda v: cfg.set_relays(
            [r.strip() for r in v.split(",") if r.strip()], require_pin=False
        ),
        "kdf_iterations": lambda v: cfg.set_kdf_iterations(int(v)),
        "kdf_mode": lambda v: cfg.set_kdf_mode(v),
        "backup_interval": lambda v: cfg.set_backup_interval(float(v)),
        "nostr_max_retries": lambda v: cfg.set_nostr_max_retries(int(v)),
        "nostr_retry_delay": lambda v: cfg.set_nostr_retry_delay(float(v)),
        "min_uppercase": lambda v: cfg.set_min_uppercase(int(v)),
        "min_lowercase": lambda v: cfg.set_min_lowercase(int(v)),
        "min_digits": lambda v: cfg.set_min_digits(int(v)),
        "min_special": lambda v: cfg.set_min_special(int(v)),
        "quick_unlock": lambda v: cfg.set_quick_unlock(
            v.lower() in ("1", "true", "yes", "y", "on")
        ),
        "verbose_timing": lambda v: cfg.set_verbose_timing(
            v.lower() in ("1", "true", "yes", "y", "on")
        ),
    }

    action = mapping.get(key)
    if action is None:
        typer.echo("Unknown key")
        raise typer.Exit(code=1)

    try:
        action(value)
    except Exception as exc:  # pragma: no cover - pass through errors
        typer.echo(f"Error: {exc}")
        raise typer.Exit(code=1)

    typer.echo("Updated")


@config_app.command("toggle-secret-mode")
def config_toggle_secret_mode(ctx: typer.Context) -> None:
    """Interactively enable or disable secret mode."""
    pm = _get_pm(ctx)
    cfg = pm.config_manager
    try:
        enabled = cfg.get_secret_mode_enabled()
        delay = cfg.get_clipboard_clear_delay()
    except Exception as exc:  # pragma: no cover - pass through errors
        typer.echo(f"Error loading settings: {exc}")
        raise typer.Exit(code=1)

    typer.echo(f"Secret mode is currently {'ON' if enabled else 'OFF'}")
    choice = (
        typer.prompt(
            "Enable secret mode? (y/n, blank to keep)", default="", show_default=False
        )
        .strip()
        .lower()
    )
    if choice in ("y", "yes"):
        enabled = True
    elif choice in ("n", "no"):
        enabled = False

    inp = typer.prompt(
        f"Clipboard clear delay in seconds [{delay}]", default="", show_default=False
    ).strip()
    if inp:
        try:
            delay = int(inp)
            if delay <= 0:
                typer.echo("Delay must be positive")
                raise typer.Exit(code=1)
        except ValueError:
            typer.echo("Invalid number")
            raise typer.Exit(code=1)

    try:
        cfg.set_secret_mode_enabled(enabled)
        cfg.set_clipboard_clear_delay(delay)
        pm.secret_mode_enabled = enabled
        pm.clipboard_clear_delay = delay
    except Exception as exc:  # pragma: no cover - pass through errors
        typer.echo(f"Error: {exc}")
        raise typer.Exit(code=1)

    status = "enabled" if enabled else "disabled"
    typer.echo(f"Secret mode {status}.")


@config_app.command("toggle-offline")
def config_toggle_offline(ctx: typer.Context) -> None:
    """Enable or disable offline mode."""
    pm = _get_pm(ctx)
    cfg = pm.config_manager
    try:
        enabled = cfg.get_offline_mode()
    except Exception as exc:  # pragma: no cover - pass through errors
        typer.echo(f"Error loading settings: {exc}")
        raise typer.Exit(code=1)

    typer.echo(f"Offline mode is currently {'ON' if enabled else 'OFF'}")
    choice = (
        typer.prompt(
            "Enable offline mode? (y/n, blank to keep)", default="", show_default=False
        )
        .strip()
        .lower()
    )
    if choice in ("y", "yes"):
        enabled = True
    elif choice in ("n", "no"):
        enabled = False

    try:
        cfg.set_offline_mode(enabled)
        pm.offline_mode = enabled
    except Exception as exc:  # pragma: no cover - pass through errors
        typer.echo(f"Error: {exc}")
        raise typer.Exit(code=1)

    status = "enabled" if enabled else "disabled"
    typer.echo(f"Offline mode {status}.")


@fingerprint_app.command("list")
def fingerprint_list(ctx: typer.Context) -> None:
    """List available seed profiles."""
    pm = _get_pm(ctx)
    for fp in pm.fingerprint_manager.list_fingerprints():
        typer.echo(fp)


@fingerprint_app.command("add")
def fingerprint_add(ctx: typer.Context) -> None:
    """Create a new seed profile."""
    pm = _get_pm(ctx)
    pm.add_new_fingerprint()


@fingerprint_app.command("remove")
def fingerprint_remove(ctx: typer.Context, fingerprint: str) -> None:
    """Remove a seed profile."""
    pm = _get_pm(ctx)
    pm.fingerprint_manager.remove_fingerprint(fingerprint)


@fingerprint_app.command("switch")
def fingerprint_switch(ctx: typer.Context, fingerprint: str) -> None:
    """Switch to another seed profile."""
    pm = _get_pm(ctx)
    pm.select_fingerprint(fingerprint)


@util_app.command("generate-password")
def generate_password(ctx: typer.Context, length: int = 24) -> None:
    """Generate a strong password."""
    pm = _get_pm(ctx)
    password = pm.password_generator.generate_password(length)
    typer.echo(password)


@util_app.command("verify-checksum")
def verify_checksum(ctx: typer.Context) -> None:
    """Verify the SeedPass script checksum."""
    pm = _get_pm(ctx)
    pm.handle_verify_checksum()


@util_app.command("update-checksum")
def update_checksum(ctx: typer.Context) -> None:
    """Regenerate the script checksum file."""
    pm = _get_pm(ctx)
    pm.handle_update_script_checksum()


@api_app.command("start")
def api_start(ctx: typer.Context, host: str = "127.0.0.1", port: int = 8000) -> None:
    """Start the SeedPass API server."""
    token = api_module.start_server(ctx.obj.get("fingerprint"))
    typer.echo(f"API token: {token}")
    uvicorn.run(api_module.app, host=host, port=port)


@api_app.command("stop")
def api_stop(ctx: typer.Context, host: str = "127.0.0.1", port: int = 8000) -> None:
    """Stop the SeedPass API server."""
    import requests

    try:
        requests.post(
            f"http://{host}:{port}/api/v1/shutdown",
            headers={"Authorization": f"Bearer {api_module._token}"},
            timeout=2,
        )
    except Exception as exc:  # pragma: no cover - best effort
        typer.echo(f"Failed to stop server: {exc}")


if __name__ == "__main__":
    app()
