"""promptkit CLI — The Python CLI for prompt engineering."""

import sys
from pathlib import Path
from typing import Optional

import click

from promptkit import __version__
from promptkit.config import Config
from promptkit.store import PromptStore
from promptkit.display import (
    console,
    print_success,
    print_error,
    print_warning,
    print_info,
    print_prompt_content,
    print_prompt_list,
    print_version_history,
    print_diff,
    print_stats,
    print_cost_estimate,
    print_init_success,
)


def _get_store() -> PromptStore:
    """Get a PromptStore instance, error if not initialized."""
    try:
        config = Config()
        if not config.is_initialized():
            print_error("No promptkit project found.")
            print_info("Run 'promptkit init' to create one.")
            sys.exit(1)
        return PromptStore(config)
    except RuntimeError as e:
        print_error(str(e))
        sys.exit(1)


@click.group()
@click.version_option(version=__version__, prog_name="promptkit")
def main():
    """promptkit — The Python CLI for prompt engineering.

    Version control, testing, linting, and CI/CD for LLM prompts.
    No Docker. No infrastructure. Just pip install promptkit.
    """
    pass


@main.command()
@click.option("--force", is_flag=True, help="Overwrite existing project")
def init(force: bool):
    """Initialize a new promptkit project."""
    cwd = Path.cwd()
    pk_dir = cwd / ".promptkit"

    if pk_dir.exists() and not force:
        print_warning("Already initialized. Use --force to reinitialize.")
        return

    if pk_dir.exists() and force:
        import shutil
        shutil.rmtree(pk_dir)

    Config.init_project(cwd)
    print_init_success(str(cwd))


@main.command()
@click.argument("name")
@click.option("--content", "-c", help="Prompt content inline")
@click.option("--file", "-f", "source_file", type=click.Path(exists=True), help="Read content from file")
@click.option("--message", "-m", default="", help="Commit message")
@click.option("--description", "-d", default="", help="Prompt description")
def save(name: str, content: Optional[str], source_file: Optional[str], message: str, description: str):
    """Save a new version of a prompt.

    Examples:
        promptkit save my-prompt -c "You are a helpful assistant."
        promptkit save my-prompt -f prompt.md -m "Updated instructions"
        echo "content" | promptkit save my-prompt
    """
    store = _get_store()

    # Get content from args, file, or stdin
    if content:
        pass
    elif source_file:
        content = Path(source_file).read_text()
    elif not sys.stdin.isatty():
        content = sys.stdin.read()
    else:
        print_error("Provide content via --content, --file, or stdin")
        sys.exit(1)

    try:
        version = store.commit(name, content, message=message, description=description)
        print_success(
            f"Saved [bold]{name}[/bold] v{version.version} "
            f"[dim]({version.short_hash})[/dim]"
        )
        if version.variables:
            print_info(f"Variables: {', '.join(version.variables)}")
    except ValueError as e:
        print_warning(str(e))


@main.command()
@click.argument("name")
@click.option("--version", "-v", "ver", type=int, help="Specific version number")
@click.option("--tag", "-t", help="Get version by tag")
@click.option("--var", multiple=True, help="Variable substitution (key=value)")
@click.option("--raw", is_flag=True, help="Output raw content (no formatting)")
def get(name: str, ver: Optional[int], tag: Optional[str], var: tuple, raw: bool):
    """Get prompt content.

    Examples:
        promptkit get my-prompt
        promptkit get my-prompt -v 2
        promptkit get my-prompt -t prod
        promptkit get my-prompt --var name=Alice --var count=5
    """
    store = _get_store()

    # Parse variables
    variables = {}
    for v in var:
        if "=" in v:
            # Support space-separated key=value pairs
            for pair in v.split():
                if "=" in pair:
                    key, value = pair.split("=", 1)
                    variables[key] = value
        else:
            print_error(f"Invalid variable format: {v} (expected key=value)")
            sys.exit(1)

    result = store.get(name, version=ver, tag=tag, variables=variables or None)
    if result is None:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)

    if raw:
        click.echo(result)
    else:
        title = f"{name}"
        if ver:
            title += f" v{ver}"
        elif tag:
            title += f" [{tag}]"
        print_prompt_content(result, title=title)


@main.command(name="list")
@click.option("--search", "-s", help="Search by name or content")
def list_prompts(search: Optional[str]):
    """List all prompts.

    Examples:
        promptkit list
        promptkit list -s "assistant"
    """
    store = _get_store()

    if search:
        prompts = store.search(search)
    else:
        prompts = store.list_prompts()

    print_prompt_list(prompts)


@main.command()
@click.argument("name")
def history(name: str):
    """Show version history for a prompt.

    Examples:
        promptkit history my-prompt
    """
    store = _get_store()
    prompt = store.load(name)
    if not prompt:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)

    print_version_history(prompt)


@main.command()
@click.argument("name")
@click.argument("v1", type=int)
@click.argument("v2", type=int)
def diff(name: str, v1: int, v2: int):
    """Compare two versions of a prompt.

    Examples:
        promptkit diff my-prompt 1 2
    """
    store = _get_store()
    try:
        diff_lines = store.diff(name, v1, v2)
        print_diff(diff_lines, name, v1, v2)
    except ValueError as e:
        print_error(str(e))
        sys.exit(1)


@main.command()
@click.argument("name")
@click.argument("tag_name")
@click.option("--version", "-v", "ver", type=int, help="Version to tag (default: latest)")
def tag(name: str, tag_name: str, ver: Optional[int]):
    """Tag a prompt version.

    Examples:
        promptkit tag my-prompt prod
        promptkit tag my-prompt staging -v 2
    """
    store = _get_store()
    try:
        store.tag(name, tag_name, ver)
        prompt = store.load(name)
        actual_ver = prompt.tags[tag_name] if prompt else ver
        print_success(f"Tagged [bold]{name}[/bold] v{actual_ver} as [yellow]{tag_name}[/yellow]")
    except ValueError as e:
        print_error(str(e))
        sys.exit(1)


@main.command()
@click.argument("name")
@click.argument("tag_name")
def untag(name: str, tag_name: str):
    """Remove a tag from a prompt.

    Examples:
        promptkit untag my-prompt staging
    """
    store = _get_store()
    try:
        if store.untag(name, tag_name):
            print_success(f"Removed tag [yellow]{tag_name}[/yellow] from [bold]{name}[/bold]")
        else:
            print_warning(f"Tag '{tag_name}' not found on '{name}'")
    except ValueError as e:
        print_error(str(e))
        sys.exit(1)


@main.command()
@click.argument("name")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
def delete(name: str, yes: bool):
    """Delete a prompt and all versions.

    Examples:
        promptkit delete my-prompt
        promptkit delete my-prompt -y
    """
    store = _get_store()

    if not yes:
        prompt = store.load(name)
        if prompt:
            click.confirm(
                f"Delete '{name}' ({prompt.version_count} versions)?",
                abort=True,
            )
        else:
            print_error(f"Prompt '{name}' not found")
            sys.exit(1)

    if store.delete(name):
        print_success(f"Deleted [bold]{name}[/bold]")
    else:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)


@main.command(name="import")
@click.argument("name")
@click.argument("source", type=click.Path(exists=True))
@click.option("--message", "-m", default="", help="Commit message")
def import_prompt(name: str, source: str, message: str):
    """Import a prompt from a file.

    Examples:
        promptkit import my-prompt prompt.md
        promptkit import my-prompt system.txt -m "From production"
    """
    store = _get_store()
    try:
        version = store.import_prompt(name, Path(source), message=message)
        print_success(
            f"Imported [bold]{name}[/bold] v{version.version} "
            f"from {source} [dim]({version.short_hash})[/dim]"
        )
    except (FileNotFoundError, ValueError) as e:
        print_error(str(e))
        sys.exit(1)


@main.command(name="export")
@click.argument("name")
@click.argument("output", type=click.Path())
@click.option("--version", "-v", "ver", type=int, help="Version to export")
def export_prompt(name: str, output: str, ver: Optional[int]):
    """Export a prompt to a file.

    Examples:
        promptkit export my-prompt output.md
        promptkit export my-prompt output.md -v 2
    """
    store = _get_store()
    try:
        store.export_prompt(name, Path(output), version=ver)
        print_success(f"Exported [bold]{name}[/bold] to {output}")
    except ValueError as e:
        print_error(str(e))
        sys.exit(1)


@main.command()
@click.argument("name")
@click.option("--model", "-m", default="gpt-4o", help="Model for cost estimation")
@click.option("--output-tokens", "-o", type=int, default=500, help="Estimated output tokens")
def cost(name: str, model: str, output_tokens: int):
    """Estimate the cost of running a prompt.

    Examples:
        promptkit cost my-prompt
        promptkit cost my-prompt -m claude-sonnet-4-20250514
        promptkit cost my-prompt -o 1000
    """
    store = _get_store()
    prompt = store.load(name)
    if not prompt or not prompt.latest:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)

    config = store.config
    pricing = config.get_pricing(model)
    if not pricing:
        print_error(f"No pricing data for model '{model}'")
        print_info("Available models: " + ", ".join(
            config.get("pricing", {}).keys()
        ))
        sys.exit(1)

    input_tokens = prompt.latest.token_estimate()
    input_cost = input_tokens * pricing["input"] / 1_000_000
    output_cost = output_tokens * pricing["output"] / 1_000_000

    print_cost_estimate(name, model, input_tokens, output_tokens, input_cost, output_cost)


@main.command()
@click.argument("name")
@click.option("--var", multiple=True, help="Variable substitution (key=value)")
@click.option("--version", "-v", "ver", type=int, help="Version to render")
@click.option("--tag", "-t", help="Tag to render")
def render(name: str, var: tuple, ver: Optional[int], tag: Optional[str]):
    """Render a prompt with variables.

    Examples:
        promptkit render my-prompt --var name=Alice --var count=5
        promptkit render my-prompt -t prod --var api_key=sk-xxx
    """
    store = _get_store()

    variables = {}
    for v in var:
        for pair in v.split():
            if "=" in pair:
                key, value = pair.split("=", 1)
                variables[key] = value

    if not variables:
        print_error("Provide at least one variable with --var key=value")
        sys.exit(1)

    result = store.get(name, version=ver, tag=tag, variables=variables)
    if result is None:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)

    print_prompt_content(result, title=f"{name} (rendered)")


@main.command()
def stats():
    """Show promptkit statistics."""
    store = _get_store()
    print_stats(store.stats())


@main.command()
@click.argument("name")
@click.option("--version", "-v", "ver", type=int, help="Version to lint")
@click.option("--json-output", is_flag=True, help="Output as JSON")
def lint(name: str, ver: Optional[int], json_output: bool):
    """Lint a prompt for quality and best practices.

    Examples:
        promptkit lint my-prompt
        promptkit lint my-prompt -v 2
        promptkit lint my-prompt --json-output
    """
    import json as json_mod
    from promptkit.linter import PromptLinter

    store = _get_store()
    prompt = store.load(name)
    if not prompt:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)

    linter = PromptLinter()
    report = linter.lint(prompt, version=ver)

    if json_output:
        click.echo(json_mod.dumps(report.to_dict(), indent=2))
    else:
        _print_lint_report(report)
        sys.exit(0 if report.passed else 1)


def _print_lint_report(report) -> None:
    """Print lint report using rich."""
    from rich.table import Table

    if not report.results:
        print_success(f"[bold]{report.prompt_name}[/bold] v{report.version} — No issues found! Score: 100/100")
        return

    table = Table(
        title=f"[bold]Lint: {report.prompt_name} v{report.version}[/bold]",
        show_header=True,
        header_style="bold cyan",
        border_style="dim",
    )
    table.add_column("Severity", style="bold", width=8)
    table.add_column("Rule", style="yellow")
    table.add_column("Message")
    table.add_column("Suggestion", style="dim")

    severity_styles = {"error": "red", "warning": "yellow", "info": "blue"}

    for r in report.results:
        sev_style = severity_styles.get(r.severity.value, "white")
        table.add_row(
            f"[{sev_style}]{r.severity.value.upper()}[/{sev_style}]",
            r.rule,
            r.message,
            r.suggestion or "",
        )

    console.print(table)
    status = "[green]PASSED[/green]" if report.passed else "[red]FAILED[/red]"
    console.print(f"\nScore: [bold]{report.score:.0f}/100[/bold] — {status}")
    console.print(
        f"[dim]{len(report.errors)} errors, {len(report.warnings)} warnings, "
        f"{len(report.infos)} info[/dim]"
    )


@main.command()
@click.argument("name")
@click.argument("test_file", type=click.Path(exists=True))
@click.option("--version", "-v", "ver", type=int, help="Version to test")
@click.option("--json-output", is_flag=True, help="Output as JSON")
def test(name: str, test_file: str, ver: Optional[int], json_output: bool):
    """Run tests against a prompt.

    Examples:
        promptkit test my-prompt tests.json
        promptkit test my-prompt tests.yaml -v 2
    """
    import json as json_mod
    from promptkit.tester import PromptTester, load_test_cases

    store = _get_store()
    prompt = store.load(name)
    if not prompt:
        print_error(f"Prompt '{name}' not found")
        sys.exit(1)

    try:
        cases = load_test_cases(Path(test_file))
    except (FileNotFoundError, ValueError, ImportError) as e:
        print_error(str(e))
        sys.exit(1)

    tester = PromptTester()
    report = tester.test(prompt, cases, version=ver)

    if json_output:
        click.echo(json_mod.dumps(report.to_dict(), indent=2))
    else:
        _print_test_report(report)
        sys.exit(0 if report.all_passed else 1)


def _print_test_report(report) -> None:
    """Print test report using rich."""
    from rich.table import Table

    table = Table(
        title=f"[bold]Tests: {report.prompt_name} v{report.version}[/bold]",
        show_header=True,
        header_style="bold cyan",
        border_style="dim",
    )
    table.add_column("Test", style="bold white")
    table.add_column("Status", width=8)
    table.add_column("Message")
    table.add_column("Time", justify="right", style="dim")

    status_icons = {
        "passed": "[green]✓ PASS[/green]",
        "failed": "[red]✗ FAIL[/red]",
        "skipped": "[yellow]○ SKIP[/yellow]",
        "error": "[red]! ERR[/red]",
    }

    for r in report.results:
        table.add_row(
            r.test_name,
            status_icons.get(r.status.value, r.status.value),
            r.message,
            f"{r.duration_ms:.1f}ms",
        )

    console.print(table)

    status = "[green]ALL PASSED[/green]" if report.all_passed else "[red]FAILURES[/red]"
    console.print(
        f"\n{status} — {report.passed} passed, {report.failed} failed, "
        f"{report.skipped} skipped, {report.errors} errors "
        f"[dim]({report.total_duration_ms:.1f}ms)[/dim]"
    )


if __name__ == "__main__":
    main()
