# Copyright 2025 Andy Vandaric
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# File: avcmt/modules/commit_generator.py
# Description: Business logic for generating AI-powered commit messages.
#              Refactored from the original commit.py as part of Task 1.1.

import subprocess
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import Any

from avcmt.ai import generate_with_ai, render_prompt
from avcmt.utils import (
    clean_ai_response,
    extract_commit_messages_from_md,
    is_recent_dry_run,
    setup_logging,
)


class CommitError(Exception):
    """Custom exception for failures during the commit generation process."""

    pass


# CLASS RENAME: CommitManager -> CommitGenerator
# The class is renamed to better reflect its role as a "generator"
# in the new modular architecture, as per the roadmap.
class CommitGenerator:
    """
    Manages the AI-powered commit generation process, from staging changes
    to creating grouped commits.
    """

    def __init__(
        self,
        dry_run: bool = False,
        push: bool = False,
        debug: bool = False,
        force_rebuild: bool = False,
        provider: str = "pollinations",
        model: str = "gemini",
        logger: Any | None = None,
        **kwargs,
    ):
        self.dry_run = dry_run
        self.push = push
        self.debug = debug
        self.force_rebuild = force_rebuild
        self.provider = provider
        self.model = model
        self.logger = logger or setup_logging("log/commit.log")
        self.kwargs = kwargs
        self.dry_run_file = Path("log") / "commit_messages_dry_run.md"

    def _run_git_command(self, command: list[str]) -> str:
        """Helper to run a git command and handle potential errors."""
        try:
            result = subprocess.run(
                command,
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
                check=True,
            )
            return result.stdout.strip()
        except subprocess.CalledProcessError as e:
            error_message = (
                f"Git command '{' '.join(e.cmd)}' failed: {e.stderr.strip()}"
            )
            self.logger.error(error_message)
            raise CommitError(error_message) from e

    def _get_changed_files(self) -> list[str]:
        # Original logic to get files from Git
        output = self._run_git_command(
            ["git", "ls-files", "--others", "--modified", "--exclude-standard"]
        )
        detected_files = [line.strip() for line in output.split("\n") if line.strip()]

        # Filter out files that do not physically exist
        existing_files = []
        for file_path_str in detected_files:
            file_path = Path(file_path_str)
            if file_path.is_file():  # Ensure it is a file and exists
                existing_files.append(file_path_str)
            else:
                self.logger.warning(f"⏩ Skipping non-existent file: {file_path_str}")
        return existing_files

    @staticmethod
    def _group_files_by_directory(files: list[str]) -> dict[str, list[str]]:
        grouped = defaultdict(list)
        for file_path in files:
            parent_dir = str(Path(file_path).parent)
            if parent_dir == ".":
                parent_dir = "root"
            grouped[parent_dir].append(file_path)
        return grouped

    def _get_diff_for_files(self, files: list[str]) -> str:
        return self._run_git_command(["git", "--no-pager", "diff", "--staged", *files])

    def _write_dry_run_header(self):
        self.dry_run_file.parent.mkdir(parents=True, exist_ok=True)
        with self.dry_run_file.open("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(self, group_name: str, commit_message: str):
        with self.dry_run_file.open("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_changes(self, files: list[str]):
        if not files:
            return
        self.logger.info(f"Staging files: {files}")
        self._run_git_command(["git", "add", *files])

    def _commit_changes(self, message: str):
        self.logger.info(f"Committing with message:\n{message}")
        self._run_git_command(["git", "commit", "-m", message])

    def _push_changes(self):
        """Pushes all commits to the active remote branch and provides next steps."""
        self.logger.info("Pushing all commits to the active remote branch...")
        self._run_git_command(["git", "push"])
        self.logger.info("✔️ All changes pushed successfully.")

        # --- Suggested Section (With Minor Improvements) ---
        self.logger.info("\n")
        self.logger.info("💡 NEXT STEP: Synchronize with CI/CD Results")
        self.logger.info(
            "Your push has likely triggered a GitHub Action (e.g., release)."
        )
        self.logger.info(
            "After the action completes, run the following command to get the updates:"
        )
        self.logger.info("    git pull origin main")

    def _prepare_for_run(self) -> list[str] | None:
        self.logger.info(
            f"Starting CommitGenerator: dry_run={self.dry_run}, push={self.push}, force_rebuild={self.force_rebuild}"
        )
        initial_files = self._get_changed_files()
        if not initial_files:
            self.logger.info("No changed files detected. Exiting.")
            return None

        if not self.dry_run:
            self.logger.info("Staging all detected changes for a consistent state...")
            self._stage_changes(initial_files)

        files_to_commit = self._run_git_command(
            ["git", "diff", "--name-only", "--cached"]
        ).split("\n")
        files_to_commit = [f for f in files_to_commit if f]

        if not files_to_commit and not self.dry_run:
            self.logger.warning("No files were staged for commit.")
            return None

        return files_to_commit or initial_files

    def _process_single_group(
        self, group_name: str, files: list[str], cached_messages: dict
    ):
        self.logger.info(f"--- Processing group: {group_name} ---")

        if self.dry_run:
            self._stage_changes(files)  # This is line 178 that triggers the error
        diff = self._get_diff_for_files(files)

        if not diff.strip():
            self.logger.info(f"[SKIP] No diff detected for {group_name}.")
            if self.dry_run:
                self._run_git_command(["git", "reset", "HEAD", "--", *files])
            return

        if not self.force_rebuild and group_name in cached_messages:
            commit_message = cached_messages[group_name]
            self.logger.info(f"[CACHED] Using cached message for {group_name}.")
        else:
            if self.force_rebuild and group_name in cached_messages:
                self.logger.info(
                    f"[FORCED] Ignoring cache and rebuilding message for {group_name}."
                )
            prompt = render_prompt(group_name, diff)
            raw_message = generate_with_ai(
                prompt,
                provider=self.provider,
                model=self.model,
                debug=self.debug,
                **self.kwargs,
            )
            commit_message = clean_ai_response(raw_message)

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

        if not commit_message.strip():
            self.logger.error(
                f"Generated commit message for group '{group_name}' is empty after cleaning. Skipping commit."
            )
            if not self.dry_run:
                self._run_git_command(["git", "reset", "HEAD", "--", *files])
            return

        if self.dry_run:
            self._write_dry_run_entry(group_name, commit_message)
            self._run_git_command(["git", "reset", "HEAD", "--", *files])
        else:
            self._commit_changes(commit_message)

    def run(self):
        files_to_commit = self._prepare_for_run()
        if not files_to_commit:
            return

        if self.force_rebuild:
            self.logger.info("--force-rebuild is active, ignoring cache.")
            cached_messages = {}
        else:
            use_cache = not self.dry_run and is_recent_dry_run(self.dry_run_file)
            cached_messages = (
                extract_commit_messages_from_md(self.dry_run_file) if use_cache else {}
            )

        grouped_files = self._group_files_by_directory(files_to_commit)

        if self.dry_run:
            self._write_dry_run_header()
        else:
            self._run_git_command(["git", "reset", "HEAD", "--", *files_to_commit])

        for group_name, files in grouped_files.items():
            if not self.dry_run:
                self._stage_changes(files)
            self._process_single_group(group_name, files, cached_messages)

        if self.push and not self.dry_run:
            self._push_changes()

        if self.dry_run:
            self.logger.info(
                f"DRY RUN COMPLETED. Review suggestions in: {self.dry_run_file}"
            )
        else:
            self.logger.info("✅ Commit process completed.")


def run_commit_group_all(**kwargs):
    """Initializes and runs the CommitGenerator."""
    try:
        # INSTANTIATION UPDATE: Use the new class name
        generator = CommitGenerator(**kwargs)
        generator.run()
    except (CommitError, Exception) as e:
        logger = setup_logging("log/commit.log")
        logger.error(f"FATAL: The commit process failed: {e}", exc_info=True)


__all__ = ["run_commit_group_all"]
