# 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
# FINAL REVISION: Smartly handles push even with no new file changes.

import logging
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
from avcmt.utils import (
    clean_ai_response,
    extract_commit_messages_from_md,
    get_jinja_env,
    is_recent_dry_run,
    setup_logging,
)


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


class CommitGenerator:
    """Manages the AI-powered commit generation process."""

    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"
        self.commit_template_env = get_jinja_env("commit")

    # ... (Metode helper lain dari _run_git_command hingga _get_commit_message tetap sama) ...
    def _run_git_command(self, command: list[str], ignore_errors: bool = False) -> str:
        try:
            result = subprocess.run(
                command,
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
                check=not ignore_errors,
            )
            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]:
        output = self._run_git_command(
            [
                "git",
                "ls-files",
                "--deleted",
                "--modified",
                "--others",
                "--exclude-standard",
            ]
        )
        return [line.strip() for line in output.split("\n") if line.strip()]

    @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 for group: {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):
        self.logger.info("Pushing all commits to the active remote branch...")
        self._run_git_command(["git", "push"])
        self.logger.info("✔️ All changes pushed successfully.")

    def _get_commit_message(
        self, group_name: str, diff: str, cached_messages: dict
    ) -> str:
        if not self.force_rebuild and group_name in cached_messages:
            self.logger.info(f"[CACHED] Using cached message for {group_name}.")
            return cached_messages[group_name]
        if self.force_rebuild and group_name in cached_messages:
            self.logger.info(f"[FORCED] Ignoring cache for {group_name}.")
        template = self.commit_template_env.get_template("commit_message.j2")
        prompt = template.render(group_name=group_name, diff_text=diff)
        raw_message = generate_with_ai(
            prompt,
            provider=self.provider,
            model=self.model,
            debug=self.debug,
            **self.kwargs,
        )
        return clean_ai_response(raw_message)

    # --- FUNGSI HELPER BARU ---
    def _is_local_ahead(self) -> bool:
        """Checks if the local branch has commits that the remote branch does not."""
        try:
            # Pastikan remote-tracking branch ada
            self._run_git_command(["git", "fetch", "origin"])
            # Hitung jumlah commit yang ada di lokal tapi tidak di remote
            output = self._run_git_command(
                ["git", "rev-list", "--count", "@{u}..HEAD"], ignore_errors=True
            )
            if output and int(output) > 0:
                self.logger.info(
                    f"Local branch is ahead of remote by {output} commit(s)."
                )
                return True
        except CommitError as e:
            # Ini bisa terjadi jika branch belum pernah di-push (tidak ada upstream)
            self.logger.warning(
                f"Could not check remote status (branch may be new): {e}"
            )
            # Anggap saja 'ahead' jika ada commit lokal tapi upstream belum ada
            return bool(
                self._run_git_command(["git", "rev-parse", "HEAD"], ignore_errors=True)
            )
        return False

    def run(self):
        """Main execution method with improved state checking."""
        initial_files = self._get_changed_files()
        local_is_ahead = self._is_local_ahead()

        # --- LOGIKA BARU: Keluar hanya jika tidak ada perubahan DAN tidak ada commit untuk di-push ---
        if not initial_files and not local_is_ahead:
            self.logger.info(
                "No changed files and local branch is up-to-date. Nothing to do."
            )
            return

        # Proses commit hanya jika ada perubahan file
        if initial_files:
            grouped_files = self._group_files_by_directory(initial_files)
            cached_messages = self._prepare_cache()
            _, failed_groups = self._process_groups(grouped_files, cached_messages)
        else:
            # Jika tidak ada file baru, tapi lokal lebih maju, lewati proses commit
            self.logger.info(
                "No new file changes to commit, but local branch is ahead. Proceeding to push."
            )
            failed_groups = []

        self._finalize_run(failed_groups)

    # ... (Metode _prepare_cache, _process_groups, _process_single_group tidak berubah dari patch sebelumnya) ...
    def _prepare_cache(self) -> dict[str, str]:
        if self.dry_run:
            self._write_dry_run_header()
            return {}
        if not self.force_rebuild and is_recent_dry_run(self.dry_run_file):
            self.logger.info(f"Recent cache found. Loading from {self.dry_run_file}")
            return extract_commit_messages_from_md(self.dry_run_file)
        return {}

    def _process_groups(
        self, grouped_files: dict, cached_messages: dict
    ) -> tuple[list, list]:
        successful_groups, failed_groups = [], []
        for group_name, files in grouped_files.items():
            was_successful = self._process_single_group(
                group_name, files, cached_messages
            )
            if was_successful:
                successful_groups.append(group_name)
            else:
                failed_groups.append(group_name)
        return successful_groups, failed_groups

    def _process_single_group(
        self, group_name: str, files: list, cached_messages: dict
    ) -> bool:
        self._stage_changes(files)
        diff = self._get_diff_for_files(files)
        if not diff.strip():
            self.logger.info(f"[SKIP] No diff for group {group_name}. Unstaging.")
            self._run_git_command(["git", "reset", "HEAD", "--", *files])
            return True
        commit_message = self._get_commit_message(group_name, diff, cached_messages)
        if not commit_message:
            self.logger.error(f"Skipping group '{group_name}' due to empty message.")
            return False
        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)
        return True

    def _finalize_run(self, failed_groups: list):
        if self.push and not self.dry_run:
            if not failed_groups:
                self._push_changes()
            else:
                self.logger.error(
                    "❌ Push aborted because one or more commit groups failed."
                )

        if self.dry_run:
            self.logger.info(
                f"✅ DRY RUN COMPLETED. Review suggestions in: {self.dry_run_file.resolve()}"
            )
        else:
            self.logger.info("✅ Commit process completed.")
            if failed_groups:
                self.logger.warning(
                    f"The following groups were skipped: {', '.join(failed_groups)}"
                )
                self.logger.warning(
                    "Please review the changes, commit them manually, and then push."
                )


def run_commit_group_all(**kwargs):
    """Initializes and runs the CommitGenerator."""
    logger = logging.getLogger("avcmt")
    try:
        generator = CommitGenerator(**kwargs)
        generator.run()
    except (CommitError, Exception) as e:
        logger.error(f"FATAL: The commit process failed: {e}", exc_info=True)


__all__ = ["run_commit_group_all"]
