"""
claudemd — CLAUDE.md Lifecycle Manager CLI

Auto-generate, maintain, and validate your CLAUDE.md from your codebase.

Commands:
    claudemd generate   Generate a CLAUDE.md from scratch
    claudemd check      Check for staleness
    claudemd update     Update stale sections
    claudemd lint       Validate quality
    claudemd diff       Compare versions
    claudemd watch      Watch for changes
    claudemd init       Initialize configuration
"""

import os
import sys
import json
from pathlib import Path
from typing import Optional

# Fix Windows UTF-8 output
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
    try:
        sys.stdout.reconfigure(encoding="utf-8")
    except Exception:
        pass
if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
    try:
        sys.stderr.reconfigure(encoding="utf-8")
    except Exception:
        pass

import click

from . import __version__
from .generators.codebase_scanner import CodebaseScanner
from .generators.stack_detector import StackDetector
from .generators.git_analyzer import GitAnalyzer
from .generators.doc_reader import DocReader
from .generators.claude_generator import ClaudeGenerator
from .analyzers.staleness_checker import StalenessChecker
from .analyzers.token_counter import TokenCounter
from .analyzers.linter import ClaudeMdLinter


def get_api_key() -> str:
    """Get Anthropic API key from environment or config file."""
    key = os.environ.get("ANTHROPIC_API_KEY", "")
    if not key:
        config = _load_config()
        key = config.get("api_key", "")
    return key


def _load_config(project_root: str = ".") -> dict:
    """Load .claudemd.toml config if it exists."""
    config = {}
    try:
        import tomllib
    except ImportError:
        try:
            import tomli as tomllib
        except ImportError:
            return config

    config_path = Path(project_root) / ".claudemd.toml"
    if config_path.exists():
        try:
            with open(config_path, "rb") as f:
                config = tomllib.load(f).get("claudemd", {})
        except Exception:
            pass
    return config


def _get_output_path(config: dict, default: str = "CLAUDE.md") -> str:
    """Get the output file path from config or default."""
    return config.get("output", default)


def _print_header():
    """Print the claudemd header."""
    try:
        from rich.console import Console
        from rich.text import Text
        console = Console()
        console.print(f"\n[bold purple]claudemd[/bold purple] [dim]v{__version__}[/dim] — CLAUDE.md Lifecycle Manager\n")
    except ImportError:
        print(f"\nclaudemd v{__version__} — CLAUDE.md Lifecycle Manager\n")


def _print_success(msg: str):
    try:
        from rich.console import Console
        Console().print(f"[bold green]✓[/bold green] {msg}")
    except ImportError:
        print(f"✓ {msg}")


def _print_error(msg: str):
    try:
        from rich.console import Console
        Console(stderr=True).print(f"[bold red]✗[/bold red] {msg}")
    except ImportError:
        print(f"✗ {msg}", file=sys.stderr)


def _print_info(msg: str):
    try:
        from rich.console import Console
        Console().print(f"[dim]{msg}[/dim]")
    except ImportError:
        print(msg)


def _print_warning(msg: str):
    try:
        from rich.console import Console
        Console().print(f"[bold yellow]⚠[/bold yellow] {msg}")
    except ImportError:
        print(f"⚠ {msg}")


def _gather_codebase_data(project_root: str, config: dict) -> tuple[dict, dict, dict, dict]:
    """Gather all codebase data needed for generation."""
    _print_info("  Scanning directory structure...")
    scanner = CodebaseScanner(project_root)
    scan_data = scanner.scan()

    _print_info("  Detecting technology stack...")
    stack_detector = StackDetector(project_root)
    stack_data = stack_detector.detect()

    _print_info("  Analyzing git history...")
    git_analyzer = GitAnalyzer(project_root)
    git_data = git_analyzer.analyze()

    _print_info("  Reading documentation...")
    doc_reader = DocReader(project_root)
    doc_data = doc_reader.analyze()

    return scan_data, stack_data, git_data, doc_data


# =============================================================================
# CLI Commands
# =============================================================================

@click.group()
@click.version_option(version=__version__, prog_name="claudemd")
@click.option("--project", "-p", default=".", help="Project root directory (default: current directory)")
@click.pass_context
def main(ctx, project):
    """
    claudemd — CLAUDE.md Lifecycle Manager

    Auto-generate, maintain, and validate your CLAUDE.md.
    Supports Claude Code, Cursor, GitHub Copilot, and Windsurf.

    Get started: claudemd generate

    Set ANTHROPIC_API_KEY before using generate or update commands.
    """
    ctx.ensure_object(dict)
    ctx.obj["project"] = os.path.abspath(project)
    ctx.obj["config"] = _load_config(project)


@main.command()
@click.option("--output", "-o", default=None, help="Output file path (default: CLAUDE.md)")
@click.option("--model", "-m", default="claude-haiku-4-5-20251001",
              type=click.Choice(["claude-haiku-4-5-20251001", "claude-sonnet-4-6", "claude-opus-4-6"]),
              help="Claude model to use")
@click.option("--max-tokens", default=2000, help="Max tokens in generated output")
@click.option("--also", multiple=True, help="Also write to these paths (e.g. --also .cursorrules)")
@click.option("--dry-run", is_flag=True, help="Print without writing to disk")
@click.option("--force", "-f", is_flag=True, help="Overwrite existing CLAUDE.md without prompting")
@click.pass_context
def generate(ctx, output, model, max_tokens, also, dry_run, force):
    """Generate a CLAUDE.md from your codebase.

    Scans your project structure, detects the tech stack, reads git history and
    existing documentation, then uses Claude to write a high-quality CLAUDE.md.

    Example:
        claudemd generate
        claudemd generate --model claude-sonnet-4-6
        claudemd generate --also .cursorrules --also .github/copilot-instructions.md
    """
    _print_header()
    project = ctx.obj["project"]
    config = ctx.obj["config"]

    output_path = output or _get_output_path(config)
    full_output = Path(project) / output_path

    # Check if file already exists
    if full_output.exists() and not force and not dry_run:
        click.echo(f"'{output_path}' already exists.")
        if not click.confirm("Overwrite?", default=False):
            _print_info("Aborted. Use --force to skip this prompt.")
            return

    # Check API key
    api_key = get_api_key()
    if not api_key:
        _print_error("No Anthropic API key found.")
        click.echo("Set ANTHROPIC_API_KEY environment variable or run: claudemd init --set-key YOUR_KEY")
        sys.exit(1)

    click.echo(f"Generating CLAUDE.md for: {Path(project).name}")
    click.echo(f"Using model: {model}\n")

    try:
        # Gather codebase data
        scan_data, stack_data, git_data, doc_data = _gather_codebase_data(project, config)

        # Show what we found
        _print_info(f"  Found: {stack_data.get('languages', ['Unknown'])} project")
        _print_info(f"  Files: {scan_data.get('stats', {}).get('total_code_files', 0)} code files")

        click.echo("\n  Generating with Claude...\n")
        generator = ClaudeGenerator(api_key=api_key, model=model)
        content = generator.generate(
            stack_data=stack_data,
            scan_data=scan_data,
            git_data=git_data,
            doc_data=doc_data,
            project_name=Path(project).name,
            max_tokens=max_tokens,
        )

        # Show token count
        counter = TokenCounter()
        tokens = counter.count(content)
        _print_info(f"  Generated: {tokens} tokens, {len(content.splitlines())} lines\n")

        if dry_run:
            click.echo("=" * 60)
            click.echo(content)
            click.echo("=" * 60)
            click.echo(f"\n(Dry run — not written to disk)")
            return

        # Write main output
        full_output.parent.mkdir(parents=True, exist_ok=True)
        full_output.write_text(content, encoding="utf-8")
        _print_success(f"Written to {output_path} ({tokens} tokens)")

        # Write additional formats
        additional = list(also) + config.get("also_generate", [])
        for extra_path in additional:
            extra_full = Path(project) / extra_path
            extra_full.parent.mkdir(parents=True, exist_ok=True)
            extra_full.write_text(content, encoding="utf-8")
            _print_success(f"Also written to {extra_path}")

        click.echo(f"\nDone! Open {output_path} to review and customize.")
        click.echo(f"Tip: Run 'claudemd lint' to check quality, or 'claudemd watch' to monitor for staleness.\n")

    except ValueError as e:
        _print_error(str(e))
        sys.exit(1)
    except Exception as e:
        _print_error(f"Generation failed: {e}")
        raise


@main.command()
@click.option("--file", "-f", default=None, help="Path to CLAUDE.md (default: auto-detect)")
@click.option("--threshold", "-t", default=None, type=int, help="Exit with code 1 if score >= this value")
@click.option("--format", "fmt", type=click.Choice(["text", "json"]), default="text")
@click.pass_context
def check(ctx, file, threshold, fmt):
    """Check your CLAUDE.md for staleness.

    Compares the file against your current codebase and reports:
    - Dead references to deleted files/directories
    - Missing references to new important files
    - Age warnings

    Returns exit code 1 if staleness score exceeds --threshold.

    Example:
        claudemd check
        claudemd check --threshold 30   # Fail in CI if >30% stale
        claudemd check --format json    # Machine-readable output
    """
    _print_header()
    project = ctx.obj["project"]
    config = ctx.obj["config"]

    claude_md_path = file or _get_output_path(config)

    checker = StalenessChecker(project, claude_md_path)
    result = checker.check()

    if fmt == "json":
        click.echo(json.dumps(result, indent=2))
        if threshold and result["score"] >= threshold:
            sys.exit(1)
        return

    click.echo(f"Checking: {claude_md_path}\n")
    report = checker.format_report(result)
    click.echo(report)

    if threshold and result["score"] >= threshold:
        click.echo(f"\nStale score {result['score']} >= threshold {threshold}. Exiting with code 1.")
        sys.exit(1)


@main.command()
@click.option("--file", "-f", default=None, help="Path to CLAUDE.md (default: auto-detect)")
@click.option("--section", "-s", default=None, help="Update only a specific section")
@click.option("--model", "-m", default="claude-haiku-4-5-20251001",
              type=click.Choice(["claude-haiku-4-5-20251001", "claude-sonnet-4-6", "claude-opus-4-6"]))
@click.option("--dry-run", is_flag=True, help="Show what would change without writing")
@click.pass_context
def update(ctx, file, section, model, dry_run):
    """Update stale sections of your CLAUDE.md.

    Runs staleness check, then uses Claude to update only the outdated sections.
    Preserves sections marked with <!-- MANUAL -->.

    Example:
        claudemd update
        claudemd update --section "Architecture"
        claudemd update --model claude-sonnet-4-6
    """
    _print_header()
    project = ctx.obj["project"]
    config = ctx.obj["config"]

    api_key = get_api_key()
    if not api_key:
        _print_error("No Anthropic API key found. Set ANTHROPIC_API_KEY.")
        sys.exit(1)

    claude_md_path = file or _get_output_path(config)
    full_path = Path(project) / claude_md_path

    if not full_path.exists():
        _print_error(f"{claude_md_path} not found. Run 'claudemd generate' first.")
        sys.exit(1)

    current_content = full_path.read_text(encoding="utf-8", errors="ignore")

    click.echo(f"Checking for staleness in {claude_md_path}...")

    if section:
        # Update a specific section
        click.echo(f"\nUpdating section: {section}")
        _print_info("  Gathering codebase context...")
        scan_data, stack_data, git_data, doc_data = _gather_codebase_data(project, config)

        stack_detector = StackDetector(project)
        context = stack_detector.to_summary()

        generator = ClaudeGenerator(api_key=api_key, model=model)
        updated = generator.update_section(
            current_content=current_content,
            section_name=section,
            change_description=f"Please update this section based on current codebase state",
            codebase_context=context,
        )
    else:
        # Run staleness check first
        checker = StalenessChecker(project, claude_md_path)
        result = checker.check()

        if result["score"] == 0:
            _print_success("CLAUDE.md is up to date! No changes needed.")
            return

        click.echo(f"Staleness score: {result['score']}/100")
        click.echo(f"Found {result['total_issues']} issues\n")

        if not result["issues"]:
            _print_success("No specific stale sections found.")
            return

        # Build stale section list for Claude
        stale_sections = []
        for issue in result["issues"]:
            if issue.get("section"):
                stale_sections.append({
                    "section": issue["section"],
                    "reason": issue["message"],
                })

        if not stale_sections:
            stale_sections = [{"section": "general", "reason": str(result["issues"][0]["message"])}]

        # Gather fresh codebase context
        _print_info("  Gathering updated codebase context...")
        scan_data, stack_data, git_data, doc_data = _gather_codebase_data(project, config)
        scanner = CodebaseScanner(project)
        context = scanner.to_summary() + "\n\n" + StackDetector(project).to_summary()

        click.echo("  Regenerating stale sections with Claude...")
        generator = ClaudeGenerator(api_key=api_key, model=model)
        updated = generator.regenerate_stale_sections(
            current_content=current_content,
            stale_sections=stale_sections,
            new_codebase_context=context,
        )

    if dry_run:
        # Show diff
        click.echo("\nProposed changes (dry run):")
        click.echo("=" * 60)
        _show_diff(current_content, updated)
        click.echo("=" * 60)
        click.echo("(Dry run — not written)")
        return

    # Show diff and confirm
    click.echo("\nProposed changes:")
    _show_diff(current_content, updated)

    if click.confirm("\nApply these changes?", default=True):
        full_path.write_text(updated, encoding="utf-8")
        tokens = TokenCounter().count(updated)
        _print_success(f"Updated {claude_md_path} ({tokens} tokens)")
    else:
        _print_info("Changes discarded.")


@main.command()
@click.option("--file", "-f", default=None, help="Path to CLAUDE.md (default: auto-detect)")
@click.option("--format", "fmt", type=click.Choice(["text", "json"]), default="text")
@click.option("--fix", is_flag=True, help="Attempt to auto-fix simple issues")
@click.pass_context
def lint(ctx, file, fmt, fix):
    """Lint your CLAUDE.md for quality issues.

    Checks for:
    - Token count (warn if > 1500)
    - Missing important sections
    - Generic/placeholder content
    - Dead file references
    - Formatting issues

    Example:
        claudemd lint
        claudemd lint --format json
    """
    _print_header()
    project = ctx.obj["project"]
    config = ctx.obj["config"]

    claude_md_path = file or _get_output_path(config)
    full_path = Path(project) / claude_md_path

    linter = ClaudeMdLinter(project)
    result = linter.lint_file(claude_md_path)

    if fmt == "json":
        click.echo(json.dumps(result, indent=2))
        if result["errors"] > 0:
            sys.exit(1)
        return

    click.echo(f"Linting: {claude_md_path}\n")
    report = linter.format_report(result)
    click.echo(report)

    # Token count details
    if full_path.exists():
        content = full_path.read_text(encoding="utf-8", errors="ignore")
        counter = TokenCounter()
        token_report = counter.format_report(content)
        click.echo(f"\n{token_report}")

    if result["errors"] > 0:
        sys.exit(1)


@main.command()
@click.option("--file", "-f", default=None, help="CLAUDE.md to scan for token usage")
@click.pass_context
def tokens(ctx, file):
    """Show token count and breakdown for CLAUDE.md."""
    _print_header()
    project = ctx.obj["project"]
    config = ctx.obj["config"]

    claude_md_path = file or _get_output_path(config)
    full_path = Path(project) / claude_md_path

    if not full_path.exists():
        _print_error(f"{claude_md_path} not found.")
        sys.exit(1)

    content = full_path.read_text(encoding="utf-8", errors="ignore")
    counter = TokenCounter()
    data = counter.analyze(content)

    click.echo(f"File: {claude_md_path}")
    click.echo(f"Total tokens: {data['total']}")
    click.echo(f"Status: {data['status'].upper()}")
    click.echo(f"Percentage of recommended (1500): {data['percentage_of_recommended']}%\n")

    if data["by_section"]:
        click.echo("Tokens by section:")
        for section, count in data["by_section"].items():
            bar = "#" * min(int(count / 10), 30)
            click.echo(f"  {section[:40]:<40} {count:>5} {bar}")


@main.command()
@click.option("--set-key", help="Set the Anthropic API key")
@click.option("--set-model", help="Set default model")
@click.pass_context
def init(ctx, set_key, set_model):
    """Initialize claudemd configuration.

    Creates a .claudemd.toml config file and sets up your API key.

    Example:
        claudemd init --set-key sk-ant-...
    """
    _print_header()
    project = ctx.obj["project"]

    config_path = Path(project) / ".claudemd.toml"

    if config_path.exists():
        click.echo(f"Config file already exists: {config_path}")
    else:
        config_content = """[claudemd]
# Output file path (default: CLAUDE.md)
output = "CLAUDE.md"

# Max tokens in generated file
max_tokens = 2000

# Also generate these formats (uncomment to enable)
# also_generate = [".cursor/rules/project.mdc", ".github/copilot-instructions.md"]

# Files/dirs to exclude from scanning
exclude = ["node_modules", ".git", "dist", "build", "__pycache__"]

# Claude model: claude-haiku-4-5-20251001 (fast/cheap), claude-sonnet-4-6 (best)
model = "claude-haiku-4-5-20251001"
"""
        config_path.write_text(config_content, encoding="utf-8")
        _print_success(f"Created {config_path}")

    if set_key:
        # Save to environment suggestion and local config
        # (We don't write the key to the config file to avoid git exposure)
        click.echo(f"\nTo set your API key securely, add to your shell profile:")
        click.echo(f'  export ANTHROPIC_API_KEY="{set_key[:10]}..."')
        click.echo(f"\nOr use it once:")
        click.echo(f'  ANTHROPIC_API_KEY="{set_key[:10]}..." claudemd generate')

    click.echo(f"\nNext steps:")
    click.echo(f"  1. Set ANTHROPIC_API_KEY environment variable")
    click.echo(f"  2. Run: claudemd generate")
    click.echo(f"  3. Review and customize the generated CLAUDE.md")


def _show_diff(old: str, new: str):
    """Show a simple line-by-line diff."""
    import difflib
    diff = list(difflib.unified_diff(
        old.splitlines(keepends=True),
        new.splitlines(keepends=True),
        fromfile="current CLAUDE.md",
        tofile="updated CLAUDE.md",
        n=2,
    ))

    if not diff:
        click.echo("No changes detected.")
        return

    try:
        from rich.console import Console
        from rich.syntax import Syntax
        console = Console()
        diff_text = "".join(diff[:100])  # Limit output
        console.print(Syntax(diff_text, "diff", theme="monokai"))
    except ImportError:
        for line in diff[:80]:
            click.echo(line, nl=False)


if __name__ == "__main__":
    main()
