# File: avcmt/commit.py
import os
import subprocess
from collections import defaultdict
from datetime import datetime

from avcmt.ai import generate_with_ai, render_prompt
from avcmt.utils import extract_commit_messages_from_md, is_recent_dry_run

MAX_DIFF_LENGTH = 12000


def get_changed_files():
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        stdout=subprocess.PIPE,
        text=True,
        encoding="utf-8",
        errors="replace",
        check=False,
    )
    files = []
    for line in result.stdout.strip().split("\n"):
        if not line:
            continue
        status, path = line[:2], line[3:]
        path = path.strip()
        if ("D" not in status and os.path.exists(path)) or (
            "??" in status and os.path.exists(path)
        ):
            files.append(path)
    return files


def group_files_by_directory(files):
    grouped = defaultdict(list)
    for file_path in files:
        parent_dir = os.path.dirname(file_path) or "root"
        grouped[parent_dir].append(file_path)
    return grouped


def get_diff_for_files(files, staged=True):
    if staged:
        result = subprocess.run(
            ["git", "--no-pager", "diff", "--staged", *files],
            stdout=subprocess.PIPE,
            text=True,
            encoding="utf-8",
            errors="replace",
            check=False,
        )
        if not result.stdout.strip():
            result = subprocess.run(
                ["git", "--no-pager", "diff", *files],
                stdout=subprocess.PIPE,
                text=True,
                encoding="utf-8",
                errors="replace",
                check=False,
            )
        return result.stdout
    result = subprocess.run(
        ["git", "--no-pager", "diff", *files],
        stdout=subprocess.PIPE,
        text=True,
        encoding="utf-8",
        errors="replace",
        check=False,
    )
    return result.stdout


def write_dry_run_header(filepath):
    with open(filepath, "w", encoding="utf-8") as f:
        ts = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S (%Z)")
        f.write("# AI Semantic Release Commit Messages (Dry Run)\n")
        f.write(f"_Last generated: {ts}_\n\n")
        f.write("Automatically generated by `avcmt --dry-run`\n\n")


def write_dry_run_entry(filepath, group_name, commit_message):
    with open(filepath, "a", encoding="utf-8") as f:
        f.write(f"## Group: `{group_name}`\n\n```md\n{commit_message}\n```\n\n---\n\n")


def stage_all_changes(logger):
    logger.info("Auto-staging all changes before commit grouping...")
    subprocess.run(["git", "add", "."], check=False)


def process_grouped_commits(
    grouped_files,
    cached_messages,
    dry_run,
    debug,
    provider,
    model,
    logger,
    dry_run_file,
    **kwargs,
):
    for group_name, group in grouped_files.items():
        commit_group(
            group_name,
            group,
            cached_messages,
            dry_run,
            debug,
            provider,
            model,
            logger,
            dry_run_file,
            **kwargs,
        )


def commit_group(
    group_name,
    files,
    cached_messages,
    dry_run,
    debug,
    provider,
    model,
    logger,
    dry_run_file,
    **kwargs,
):
    diff = get_diff_for_files(files)
    if not diff.strip():
        logger.info(f"[SKIP] No changes detected for {group_name}.")
        return

    if group_name in cached_messages:
        commit_message = cached_messages[group_name]
        logger.info(f"[CACHED] Using cached message for {group_name}: {commit_message}")
    else:
        prompt = render_prompt(group_name, diff)
        commit_message = generate_with_ai(
            prompt, provider=provider, model=model, debug=debug, **kwargs
        )

    logger.info(f"Suggested commit message for {group_name}:\n{commit_message}\n")

    if dry_run:
        write_dry_run_entry(dry_run_file, group_name, commit_message)
    else:
        subprocess.run(["git", "add", *files], check=False)
        subprocess.run(["git", "commit", "-m", commit_message], check=False)


def handle_catchall_commit(cached_messages, debug, provider, model, logger, **kwargs):
    remaining_files = get_changed_files()
    if not remaining_files:
        return

    logger.info(f"[REMAINING] Some files are still staged: {remaining_files}")
    subprocess.run(["git", "add", *remaining_files], check=False)
    catchall_diff = get_diff_for_files(remaining_files)

    if catchall_diff and len(catchall_diff) < MAX_DIFF_LENGTH:
        if "catch-all" in cached_messages:
            catchall_message = cached_messages["catch-all"]
            logger.info(
                f"[CACHED] Using cached message for catch-all: {catchall_message}"
            )
        else:
            catchall_prompt = render_prompt("catch-all", catchall_diff)
            catchall_message = generate_with_ai(
                catchall_prompt, provider=provider, model=model, debug=debug, **kwargs
            )
    else:
        catchall_message = (
            "chore: catch-all commit for files missed by grouped auto-commit\n\n"
            "Auto-commit for any files that were staged or modified after group commits."
        )

    subprocess.run(["git", "commit", "-m", catchall_message], check=False)


def push_all_commits(logger):
    logger.info("Pushing all commits to the active remote branch...")
    result = subprocess.run(
        ["git", "push"], check=False, capture_output=True, text=True
    )
    if result.returncode != 0:
        logger.error(f"Git push failed: {result.stderr}")
    else:
        logger.info("✔️ All changes pushed to the remote branch.")


def warn_unstaged_files(logger):
    unstaged = get_changed_files()
    if unstaged:
        logger.warning(f"Still unstaged/modified files remain: {unstaged}")


def run_commit_group_all(
    dry_run=False,
    push=False,
    debug=False,
    provider="pollinations",
    model="gemini",
    logger=None,
    **kwargs,
):
    if logger:
        logger.info(
            f"Starting run_commit_group_all: dry_run={dry_run}, push={push}, debug={debug}"
        )

    if not dry_run:
        stage_all_changes(logger)

    files = get_changed_files()
    if not files:
        logger.info("No changed files detected.")
        return

    os.makedirs("log", exist_ok=True)
    dry_run_file = os.path.join("log", "commit_messages_dry_run.md")
    use_cache = not dry_run and is_recent_dry_run(dry_run_file)
    cached_messages = extract_commit_messages_from_md(dry_run_file) if use_cache else {}
    grouped_files = group_files_by_directory(files)

    if dry_run:
        write_dry_run_header(dry_run_file)

    process_grouped_commits(
        grouped_files,
        cached_messages,
        dry_run,
        debug,
        provider,
        model,
        logger,
        dry_run_file,
        **kwargs,
    )

    if not dry_run:
        handle_catchall_commit(
            cached_messages,
            debug,
            provider,
            model,
            logger,
            **kwargs,
        )

    if push and not dry_run:
        push_all_commits(logger)

    warn_unstaged_files(logger)

    if dry_run:
        logger.info(
            f"[DRY RUN COMPLETED] All commit messages are saved to: {dry_run_file}"
        )


__all__ = ["run_commit_group_all"]
