"""
SafeConfig CLI — ``safeconfig``

Entry point for the command-line tool.  All commands are grouped under the
``safeconfig`` command (registered in pyproject.toml as a console script).

Commands
--------
init        Scaffold a new .env file and generate a master key.
set         Persist a key-value pair to the .env file.
get         Print the (decrypted) value of a key.
encrypt     Encrypt a plaintext value and store the blob in .env.
doctor      Diagnose common configuration issues.
"""

from __future__ import annotations

import logging
import os
import sys
from pathlib import Path

import click
from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
from rich import print as rprint

from pysafeconfigx.core.config import MASTER_KEY_ENV_VAR, SafeConfig
from pysafeconfigx.core.crypto import decrypt_value, encrypt_value, generate_master_key, is_encrypted_blob
from pysafeconfigx.core.exceptions import (
    ConfigurationError,
    DecryptionError,
    EncryptionError,
    KeyNotFoundError,
    MasterKeyError,
)
from pysafeconfigx.utils.env_writer import create_env_template, upsert_key
from pysafeconfigx.utils.scanner import scan_directory

console = Console()
err_console = Console(stderr=True, style="bold red")

logging.basicConfig(
    level=logging.WARNING,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)


# ─── Root group ──────────────────────────────────────────────────────────────


@click.group()
@click.option(
    "--env-file",
    default=".env",
    show_default=True,
    help="Path to the .env file to operate on.",
    envvar="SAFECONFIG_ENV_FILE",
)
@click.option(
    "--verbose", "-v",
    is_flag=True,
    default=False,
    help="Enable verbose logging.",
)
@click.pass_context
def cli(ctx: click.Context, env_file: str, verbose: bool) -> None:
    """SafeConfig — secure API key and configuration management.

    \b
    Examples:
      safeconfig init
      safeconfig set DATABASE_URL postgres://localhost/mydb
      safeconfig get DATABASE_URL
      safeconfig encrypt SECRET_API_KEY
      safeconfig doctor
    """
    ctx.ensure_object(dict)
    ctx.obj["env_file"] = env_file

    if verbose:
        logging.getLogger("pysafeconfigx").setLevel(logging.DEBUG)


# ─── init ────────────────────────────────────────────────────────────────────


@cli.command()
@click.option(
    "--keys",
    "-k",
    multiple=True,
    help="Required key names to scaffold in the .env template.  "
         "May be repeated: -k DATABASE_URL -k API_KEY",
)
@click.option(
    "--force",
    is_flag=True,
    default=False,
    help="Overwrite an existing .env file.",
)
@click.pass_context
def init(ctx: click.Context, keys: tuple[str, ...], force: bool) -> None:
    """Scaffold a .env file and generate a master encryption key.

    Creates a commented .env template and prints a new SAFECONFIG_MASTER_KEY
    that you should add to your .env or secret store.
    """
    env_file = ctx.obj["env_file"]

    # Generate and display the master key first so the user can save it.
    master_key = generate_master_key()
    console.print()
    console.print(Panel(
        f"[bold green]Your new master key[/bold green]\n\n"
        f"[yellow]{master_key}[/yellow]\n\n"
        "[dim]Store this securely — it cannot be recovered if lost.[/dim]",
        title="🔑  Master Key",
        border_style="green",
    ))
    console.print()

    # Write the .env template.
    all_keys = list(keys) + [MASTER_KEY_ENV_VAR]
    try:
        create_env_template(env_file, all_keys, overwrite=force)
    except FileExistsError:
        err_console.print(
            f"[bold red]Error:[/bold red] '{env_file}' already exists.  "
            "Use --force to overwrite."
        )
        sys.exit(1)

    # Immediately write the master key into the new file.
    upsert_key(env_file, MASTER_KEY_ENV_VAR, master_key)

    console.print(f"[green]✔[/green]  Created [bold]{env_file}[/bold]")
    console.print(
        f"\n[dim]Next steps:[/dim]\n"
        f"  1. Add '{env_file}' to your [bold].gitignore[/bold]\n"
        f"  2. Fill in your keys: [bold]safeconfig set KEY value[/bold]\n"
        f"  3. Run [bold]safeconfig doctor[/bold] to verify your setup\n"
    )


# ─── set ─────────────────────────────────────────────────────────────────────


@cli.command(name="set")
@click.argument("key")
@click.argument("value")
@click.option(
    "--encrypt/--no-encrypt",
    default=False,
    help="Encrypt the value before storing.",
)
@click.pass_context
def set_command(ctx: click.Context, key: str, value: str, encrypt: bool) -> None:
    """Set KEY to VALUE in the .env file.

    \b
    Examples:
      safeconfig set DATABASE_URL postgres://localhost/mydb
      safeconfig set SECRET_KEY my_very_secret_key --encrypt
    """
    env_file = ctx.obj["env_file"]

    if encrypt:
        master_key = _require_master_key()
        try:
            blob = encrypt_value(value, master_key, config_key=key)
        except EncryptionError as exc:
            err_console.print(f"[bold red]Encryption failed:[/bold red] {exc}")
            sys.exit(1)

        stored_value = f"enc:{blob}"
        upsert_key(env_file, key, stored_value)
        console.print(f"[green]✔[/green]  Encrypted and stored [bold]{key}[/bold] in [bold]{env_file}[/bold]")
    else:
        upsert_key(env_file, key, value)
        console.print(f"[green]✔[/green]  Stored [bold]{key}[/bold] in [bold]{env_file}[/bold]")


# ─── get ─────────────────────────────────────────────────────────────────────


@cli.command(name="get")
@click.argument("key")
@click.option(
    "--decrypt/--no-decrypt",
    default=True,
    help="Decrypt the value if it is an encrypted blob (default: True).",
)
@click.option(
    "--raw",
    is_flag=True,
    default=False,
    help="Print the raw stored value (including enc: prefix) without decrypting.",
)
@click.pass_context
def get_command(ctx: click.Context, key: str, decrypt: bool, raw: bool) -> None:
    """Print the value of KEY from the .env file.

    \b
    Examples:
      safeconfig get DATABASE_URL
      safeconfig get SECRET_KEY
      safeconfig get SECRET_KEY --raw   # print encrypted blob
    """
    env_file = ctx.obj["env_file"]

    try:
        config = SafeConfig(auto_load_env=False)
        config.add_loader(__import__("safeconfig.core.loaders", fromlist=["DotEnvLoader"]).DotEnvLoader(env_file))
        config.load()
    except ConfigurationError as exc:
        err_console.print(f"[bold red]Error loading config:[/bold red] {exc}")
        sys.exit(1)

    all_data = config.all()
    stored = all_data.get(key)

    if stored is None:
        err_console.print(f"[bold red]Key not found:[/bold red] '{key}'")
        sys.exit(1)

    if raw:
        click.echo(stored)
        return

    # Auto-decrypt if the value is an encrypted blob.
    is_enc = stored.startswith("enc:") or is_encrypted_blob(stored)
    if is_enc and decrypt and not raw:
        master_key = _require_master_key()
        blob = stored[len("enc:"):] if stored.startswith("enc:") else stored
        try:
            value = decrypt_value(blob, master_key, config_key=key)
        except DecryptionError as exc:
            err_console.print(f"[bold red]Decryption failed:[/bold red] {exc}")
            sys.exit(1)
        click.echo(value)
    else:
        click.echo(stored)


# ─── encrypt ─────────────────────────────────────────────────────────────────


@cli.command()
@click.argument("key")
@click.option(
    "--in-place/--no-in-place",
    default=True,
    help="Update the .env file with the encrypted blob.",
)
@click.pass_context
def encrypt(ctx: click.Context, key: str, in_place: bool) -> None:
    """Encrypt an existing plaintext value in the .env file.

    Reads the current (plaintext) value, encrypts it, and optionally writes
    the blob back to the .env file.

    \b
    Example:
      safeconfig encrypt SECRET_API_KEY
    """
    env_file = ctx.obj["env_file"]
    master_key = _require_master_key()

    from pysafeconfigx.core.loaders import DotEnvLoader
    loader = DotEnvLoader(env_file)
    data = loader.load()

    if key not in data:
        err_console.print(f"[bold red]Key not found:[/bold red] '{key}' not in '{env_file}'")
        sys.exit(1)

    current_value = data[key]

    if current_value.startswith("enc:") or is_encrypted_blob(current_value):
        console.print(f"[yellow]⚠[/yellow]  Key [bold]{key}[/bold] is already encrypted.")
        return

    try:
        blob = encrypt_value(current_value, master_key, config_key=key)
    except EncryptionError as exc:
        err_console.print(f"[bold red]Encryption failed:[/bold red] {exc}")
        sys.exit(1)

    stored = f"enc:{blob}"

    if in_place:
        upsert_key(env_file, key, stored)
        console.print(f"[green]✔[/green]  Encrypted [bold]{key}[/bold] in [bold]{env_file}[/bold]")
    else:
        console.print(f"[bold]Encrypted blob for {key}:[/bold]")
        click.echo(stored)


# ─── doctor ──────────────────────────────────────────────────────────────────


@cli.command()
@click.option(
    "--scan-dir",
    default=".",
    show_default=True,
    help="Directory to scan for exposed secrets in source code.",
)
@click.pass_context
def doctor(ctx: click.Context, scan_dir: str) -> None:
    """Diagnose common configuration issues.

    Checks performed:
    \b
      1. Master key is set and valid
      2. .env file exists
      3. SAFECONFIG_MASTER_KEY is not committed to .env (if file is tracked)
      4. Source code scan for hard-coded secrets
    """
    env_file = ctx.obj["env_file"]
    issues: list[str] = []
    warnings_list: list[str] = []

    console.print()
    console.print(Panel("[bold]SafeConfig Doctor[/bold]", border_style="blue"))
    console.print()

    table = Table(show_header=True, header_style="bold cyan")
    table.add_column("Check", style="dim", width=40)
    table.add_column("Status", width=10)
    table.add_column("Detail")

    # ── Check 1: Master key ───────────────────────────────────────────────
    master_key_env = os.environ.get(MASTER_KEY_ENV_VAR)
    if master_key_env:
        try:
            from pysafeconfigx.core.crypto import load_master_key
            load_master_key(master_key_env)
            table.add_row("Master key (env var)", "[green]✔ OK[/green]", "Valid key found in environment")
        except MasterKeyError as exc:
            table.add_row("Master key (env var)", "[red]✘ FAIL[/red]", str(exc))
            issues.append(str(exc))
    else:
        table.add_row(
            "Master key (env var)",
            "[yellow]⚠ WARN[/yellow]",
            f"'{MASTER_KEY_ENV_VAR}' not set in environment",
        )
        warnings_list.append(f"Set {MASTER_KEY_ENV_VAR} to enable encryption features.")

    # ── Check 2: .env file ────────────────────────────────────────────────
    env_path = Path(env_file)
    if env_path.exists():
        table.add_row(f".env file ({env_file})", "[green]✔ OK[/green]", "File found")
    else:
        table.add_row(
            f".env file ({env_file})",
            "[yellow]⚠ WARN[/yellow]",
            f"'{env_file}' not found — run 'safeconfig init'",
        )
        warnings_list.append(f"'{env_file}' not found.")

    # ── Check 3: .gitignore ───────────────────────────────────────────────
    gitignore = Path(".gitignore")
    if gitignore.exists():
        content = gitignore.read_text(encoding="utf-8")
        patterns = [".env", "*.env", ".env*"]
        covered = any(p in content for p in patterns)
        if covered:
            table.add_row(".gitignore", "[green]✔ OK[/green]", ".env is ignored")
        else:
            table.add_row(
                ".gitignore",
                "[yellow]⚠ WARN[/yellow]",
                ".env may not be in .gitignore — add '.env' to be safe",
            )
            warnings_list.append("Add '.env' to your .gitignore.")
    else:
        table.add_row(".gitignore", "[yellow]⚠ WARN[/yellow]", "No .gitignore found")
        warnings_list.append("Create a .gitignore and add '.env'.")

    # ── Check 4: Source code scan ─────────────────────────────────────────
    scan_path = Path(scan_dir)
    if scan_path.exists():
        scan_result = scan_directory(scan_path, emit_warnings=False)
        if scan_result.has_issues:
            table.add_row(
                "Source code scan",
                "[red]✘ ISSUES[/red]",
                f"{len(scan_result.findings)} potential secret(s) found",
            )
            for finding in scan_result.findings[:5]:
                warnings_list.append(f"Possible exposed secret: {finding}")
        else:
            table.add_row(
                "Source code scan",
                "[green]✔ OK[/green]",
                f"No exposed secrets in {scan_result.scanned_files} file(s)",
            )
    else:
        table.add_row("Source code scan", "[dim]SKIP[/dim]", f"Directory '{scan_dir}' not found")

    console.print(table)
    console.print()

    if warnings_list:
        console.print("[yellow]Warnings:[/yellow]")
        for w in warnings_list:
            console.print(f"  [yellow]⚠[/yellow]  {w}")
        console.print()

    if issues:
        console.print(f"[red]Doctor found {len(issues)} error(s).  Please fix them before deploying.[/red]")
        sys.exit(1)
    elif warnings_list:
        console.print("[yellow]Doctor found warnings.  Review them above.[/yellow]")
    else:
        console.print("[green]All checks passed! Your configuration looks healthy.[/green]")


# ─── Helpers ─────────────────────────────────────────────────────────────────


def _require_master_key() -> str:
    """Return the master key from the environment or abort with a helpful message."""
    key = os.environ.get(MASTER_KEY_ENV_VAR)
    if not key:
        err_console.print(
            f"[bold red]Error:[/bold red] '{MASTER_KEY_ENV_VAR}' is not set.\n"
            "Generate a new key with: [bold]safeconfig init[/bold]\n"
            "Then export it: [bold]export SAFECONFIG_MASTER_KEY=<key>[/bold]"
        )
        sys.exit(1)
    return key


if __name__ == "__main__":
    cli()
