#!/usr/bin/env python3
"""
Dev CLI - Chromium patch management tool

A git-like patch management system for maintaining patches against Chromium.
Enables extracting, applying, and managing patches across Chromium upgrades.
"""

import yaml
from pathlib import Path
from typing import Optional

import typer
from typer import Typer, Option, Argument

# Import from common and utils
from ..common.context import Context
from ..common.utils import log_info, log_error, log_success, log_warning


def create_build_context(chromium_src: Optional[Path] = None) -> Optional[Context]:
    """Create BuildContext for dev CLI operations"""
    try:
        if not chromium_src:
            log_error("Chromium source directory not specified")
            log_info(
                "Use --chromium-src option to specify the Chromium source directory"
            )
            return None

        if not chromium_src.exists():
            log_error(f"Chromium source directory does not exist: {chromium_src}")
            return None

        ctx = Context(
            chromium_src=chromium_src,
            architecture="",  # Not needed for patch operations
            build_type="debug",  # Not needed for patch operations
        )

        return ctx
    except Exception as e:
        log_error(f"Failed to create build context: {e}")
        return None


# Create the Typer app
app = Typer(
    name="dev",
    help="BrowserOS dev CLI",
    no_args_is_help=True,
    pretty_exceptions_enable=False,
    pretty_exceptions_show_locals=False,
)


# State class to hold global options
class State:
    def __init__(self):
        self.chromium_src: Optional[Path] = None
        self.verbose: bool = False
        self.quiet: bool = False


state = State()


@app.callback()
def main(
    chromium_src: Optional[Path] = Option(
        None,
        "--chromium-src",
        "-S",
        help="Path to Chromium source directory",
        exists=True,
    ),
    verbose: bool = Option(False, "--verbose", "-v", help="Enable verbose output"),
    quiet: bool = Option(False, "--quiet", "-q", help="Suppress non-essential output"),
):
    """
    Dev CLI - Chromium patch management tool

    This tool provides git-like commands for managing patches against Chromium:

    Extract patches from commits:
      browseros dev extract commit HEAD
      browseros dev extract range HEAD~5 HEAD

    Apply patches:
      browseros dev apply all
      browseros dev apply feature llm-chat

    Manage features:
      browseros dev feature list
      browseros dev feature add my-feature HEAD
      browseros dev feature show my-feature
    """
    state.chromium_src = chromium_src
    state.verbose = verbose
    state.quiet = quiet


@app.command()
def status():
    """Show dev CLI status"""
    log_info("Dev CLI Status")
    log_info("-" * 40)

    build_ctx = create_build_context(state.chromium_src)
    if build_ctx:
        log_success(f"Chromium source: {build_ctx.chromium_src}")

        # Check for patches directory
        patches_dir = build_ctx.root_dir / "chromium_patches"
        if patches_dir.exists():
            patch_count = len(list(patches_dir.rglob("*.patch")))
            log_info(f"Individual patches: {patch_count}")
        else:
            log_warning("No patches directory found")

        # Check for features.yaml
        features_file = build_ctx.root_dir / "features.yaml"
        if features_file.exists():
            with open(features_file) as f:
                features = yaml.safe_load(f)
                feature_count = len(features.get("features", {}))
                log_info(f"Features defined: {feature_count}")
        else:
            log_warning("No features.yaml found")
    else:
        log_error("Failed to create build context")


# Create sub-apps for extract, apply, and feature commands
extract_app = Typer(
    name="extract",
    help="Extract patches from commits",
    pretty_exceptions_enable=False,
    pretty_exceptions_show_locals=False,
)
apply_app = Typer(
    name="apply",
    help="Apply patches to Chromium",
    pretty_exceptions_enable=False,
    pretty_exceptions_show_locals=False,
)
feature_app = Typer(
    name="feature",
    help="Manage features",
    pretty_exceptions_enable=False,
    pretty_exceptions_show_locals=False,
)

# Add sub-apps to main app
app.add_typer(extract_app, name="extract")
app.add_typer(apply_app, name="apply")
app.add_typer(feature_app, name="feature")


# Extract commands
@extract_app.command(name="commit")
def extract_commit(
    commit: str = Argument(..., help="Git commit reference (e.g., HEAD)"),
    output: Optional[Path] = Option(None, "--output", "-o", help="Output directory"),
    interactive: bool = Option(
        True, "--interactive/--no-interactive", "-i/-n", help="Interactive mode"
    ),
    force: bool = Option(False, "--force", "-f", help="Overwrite existing patches"),
    include_binary: bool = Option(False, "--include-binary", help="Include binary files"),
    base: Optional[str] = Option(
        None, "--base", help="Extract full diff from base commit for files in COMMIT"
    ),
    feature: bool = Option(
        False, "--feature", help="Add extracted files to a feature in features.yaml"
    ),
):
    """Extract patches from a single commit"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.extract import ExtractCommitModule

    module = ExtractCommitModule()
    try:
        module.validate(ctx)
        module.execute(
            ctx,
            commit=commit,
            output=output,
            interactive=interactive,
            verbose=state.verbose,
            force=force,
            include_binary=include_binary,
            base=base,
            feature=feature,
        )
    except Exception as e:
        log_error(f"Failed to extract commit: {e}")
        raise typer.Exit(1)


@extract_app.command(name="patch")
def extract_patch_cmd(
    chromium_path: str = Argument(..., help="Chromium file path (e.g., chrome/common/foo.h)"),
    base: str = Option(..., "--base", "-b", help="Base commit to diff against"),
    force: bool = Option(False, "--force", "-f", help="Overwrite existing patch without prompting"),
    feature: bool = Option(
        False, "--feature", help="Add extracted file to a feature in features.yaml"
    ),
):
    """Extract patch for a specific file"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.extract import extract_single_file_patch

    success, error = extract_single_file_patch(ctx, chromium_path, base, force)
    if not success:
        log_error(error or "Unknown error")
        raise typer.Exit(1)
    log_success(f"Successfully extracted patch for: {chromium_path}")

    # Handle --feature flag
    if feature:
        from ..modules.feature import prompt_feature_selection, add_files_to_feature

        result = prompt_feature_selection(ctx, base[:12], None)
        if result is None:
            log_warning("Skipped adding file to feature")
        else:
            feature_name, description = result
            add_files_to_feature(ctx, feature_name, description, [chromium_path])


@extract_app.command(name="range")
def extract_range(
    start: str = Argument(..., help="Start commit (exclusive)"),
    end: str = Argument(..., help="End commit (inclusive)"),
    output: Optional[Path] = Option(None, "--output", "-o", help="Output directory"),
    interactive: bool = Option(
        True, "--interactive/--no-interactive", "-i/-n", help="Interactive mode"
    ),
    force: bool = Option(False, "--force", "-f", help="Overwrite existing patches"),
    include_binary: bool = Option(False, "--include-binary", help="Include binary files"),
    squash: bool = Option(False, "--squash", help="Squash all commits into single patches"),
    base: Optional[str] = Option(
        None,
        "--base",
        help="Use different base for diff (full diff from base for files in range)",
    ),
    feature: bool = Option(
        False, "--feature", help="Add extracted files to a feature in features.yaml"
    ),
):
    """Extract patches from a range of commits"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.extract import ExtractRangeModule

    module = ExtractRangeModule()
    try:
        module.validate(ctx)
        module.execute(
            ctx,
            start=start,
            end=end,
            output=output,
            interactive=interactive,
            verbose=state.verbose,
            force=force,
            include_binary=include_binary,
            squash=squash,
            base=base,
            feature=feature,
        )
    except Exception as e:
        log_error(f"Failed to extract range: {e}")
        raise typer.Exit(1)


# Apply commands
@apply_app.command(name="all")
def apply_all(
    interactive: bool = Option(
        True, "--interactive/--no-interactive", "-i/-n", help="Interactive mode"
    ),
    reset_to: Optional[str] = Option(
        None, "--reset-to", "-r", help="Reset files to this commit before applying patches"
    ),
    annotate: bool = Option(
        False, "--annotate", "-a", help="Create git commits per feature after applying"
    ),
):
    """Apply all patches from chromium_patches/"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.apply import ApplyAllModule

    module = ApplyAllModule()
    try:
        module.validate(ctx)
        module.execute(ctx, interactive=interactive, reset_to=reset_to, annotate=annotate)
    except Exception as e:
        log_error(f"Failed to apply patches: {e}")
        raise typer.Exit(1)


@apply_app.command(name="feature")
def apply_feature(
    feature_name: str = Argument(..., help="Feature name to apply"),
    interactive: bool = Option(
        True, "--interactive/--no-interactive", "-i/-n", help="Interactive mode"
    ),
    reset_to: Optional[str] = Option(
        None, "--reset-to", "-r", help="Reset files to this commit before applying patches"
    ),
    annotate: bool = Option(
        False, "--annotate", "-a", help="Create git commit for this feature after applying"
    ),
):
    """Apply patches for a specific feature"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.apply import ApplyFeatureModule

    module = ApplyFeatureModule()
    try:
        module.validate(ctx)
        module.execute(
            ctx, feature_name=feature_name, interactive=interactive, reset_to=reset_to, annotate=annotate
        )
    except Exception as e:
        log_error(f"Failed to apply feature: {e}")
        raise typer.Exit(1)


@apply_app.command(name="patch")
def apply_patch_cmd(
    chromium_path: str = Argument(..., help="Chromium file path (e.g., chrome/common/foo.h)"),
    reset_to: Optional[str] = Option(
        None, "--reset-to", "-r", help="Reset file to this commit before applying patch"
    ),
    dry_run: bool = Option(False, "--dry-run", help="Test without applying"),
):
    """Apply patch for a specific file"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.apply import apply_single_file_patch

    success, error = apply_single_file_patch(ctx, chromium_path, reset_to, dry_run)
    if not success:
        log_error(error or "Unknown error")
        raise typer.Exit(1)
    log_success(f"Successfully applied patch for: {chromium_path}")


@apply_app.command(name="force")
def apply_force(
    reset_to: Optional[str] = Option(
        None, "--reset-to", "-r", help="Reset files to this commit before applying patches"
    ),
):
    """Apply all patches non-interactively, writing .rej files for conflicts.

    Applies every patch without prompting. When a patch conflicts, uses
    git apply --reject to apply what it can and write .rej files for
    failed hunks, then continues to the next patch.

    Examples:
        browseros dev apply force -S /chromium
        browseros dev apply force --reset-to base -S /chromium
    """
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.apply import ApplyForceModule

    module = ApplyForceModule()
    try:
        module.validate(ctx)
        module.execute(ctx, reset_to=reset_to)
    except Exception as e:
        log_error(f"Failed to apply patches: {e}")
        raise typer.Exit(1)


@apply_app.command(name="changed")
def apply_changed(
    commit: Optional[str] = Option(
        None, "--commit", "-c", help="Single commit hash to get changed patches from"
    ),
    range_start: Optional[str] = Option(
        None, "--range-start", help="Start commit of range (exclusive)"
    ),
    range_end: Optional[str] = Option(
        None, "--range-end", help="End commit of range (inclusive)"
    ),
    reset_to: str = Option(
        ..., "--reset-to", "-r", help="Reset chromium files to this commit before applying (required)"
    ),
    dry_run: bool = Option(False, "--dry-run", help="Preview changes without applying"),
):
    """Apply only patches that changed in specific commits.

    Useful for testing changes on another machine without full rebuild.
    Only resets and applies files that were modified in the specified commits.

    Examples:
        # Apply patches changed in a single commit
        browseros dev apply changed --commit 1c78477 --reset-to base -S /chromium

        # Apply patches changed in a range of commits
        browseros dev apply changed --range-start HEAD~5 --range-end HEAD --reset-to base -S /chromium
    """
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.apply import ApplyChangedModule

    module = ApplyChangedModule()
    try:
        module.validate(ctx)
        module.execute(
            ctx,
            reset_to=reset_to,
            commit=commit,
            range_start=range_start,
            range_end=range_end,
            dry_run=dry_run,
        )
    except Exception as e:
        log_error(f"Failed to apply changed patches: {e}")
        raise typer.Exit(1)


# Feature commands
@feature_app.command(name="list")
def feature_list():
    """List all defined features"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.feature import ListFeaturesModule

    module = ListFeaturesModule()
    try:
        module.validate(ctx)
        module.execute(ctx)
    except Exception as e:
        log_error(f"Failed to list features: {e}")
        raise typer.Exit(1)


@feature_app.command(name="show")
def feature_show(
    feature_name: str = Argument(..., help="Feature name to show"),
):
    """Show details of a specific feature"""
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.feature import ShowFeatureModule

    module = ShowFeatureModule()
    try:
        module.validate(ctx)
        module.execute(ctx, feature_name=feature_name)
    except Exception as e:
        log_error(f"Failed to show feature: {e}")
        raise typer.Exit(1)


@feature_app.command(name="add-update")
def feature_add_update(
    name: str = Option(
        ..., "--name", "-n", help="Feature name (lowercase kebab-case, e.g., 'llm-chat')"
    ),
    commit: str = Option(..., "--commit", "-c", help="Git commit reference"),
    description: str = Option(
        ...,
        "--description",
        "-d",
        help="Feature description with prefix (feat:, fix:, build:, chore:, series:)",
    ),
):
    """Add or update a feature with files from a commit.

    If the feature exists, merges new files into it.
    If the feature is new, creates it.

    Examples:
        browseros dev feature add-update --name llm-chat --commit HEAD -d "feat: LLM chat panel"
        browseros dev feature add-update -n api -c HEAD~3 -d "feat: browseros API updates"
    """
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.feature import AddUpdateFeatureModule

    module = AddUpdateFeatureModule()
    try:
        module.validate(ctx)
        module.execute(ctx, name=name, commit=commit, description=description)
    except Exception as e:
        log_error(f"Failed to add/update feature: {e}")
        raise typer.Exit(1)


@feature_app.command(name="classify")
def feature_classify():
    """Classify unclassified patch files into features

    Lists all patches in chromium_patches/ that are not in any feature,
    then prompts one-by-one to assign each to a feature.

    Examples:
        browseros dev feature classify
    """
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.feature import ClassifyFeaturesModule

    module = ClassifyFeaturesModule()
    try:
        module.validate(ctx)
        module.execute(ctx)
    except Exception as e:
        log_error(f"Failed to classify features: {e}")
        raise typer.Exit(1)


# Annotate command
@app.command(name="annotate")
def annotate_cmd(
    feature_name: Optional[str] = Argument(
        None, help="Optional: specific feature to annotate (default: all features)"
    ),
):
    """Create git commits organized by features from features.yaml

    For each feature with modified files, creates a commit with the format:
    "{feature_name}: {description}"

    Examples:
        browseros dev annotate -S /path/to/chromium
        browseros dev annotate llm-chat -S /path/to/chromium
    """
    ctx = create_build_context(state.chromium_src)
    if not ctx:
        raise typer.Exit(1)

    from ..modules.annotate import AnnotateModule

    module = AnnotateModule()
    try:
        module.validate(ctx)
        module.execute(ctx, feature_name=feature_name)
    except Exception as e:
        log_error(f"Failed to annotate: {e}")
        raise typer.Exit(1)


if __name__ == "__main__":
    app()
