#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = ["openai", "python-dotenv"]
# ///

# alternative shebang without uv
#!/usr/bin/env python3

# Usage:    uv run git_commit.py
# Replaces: git commit -a -m 'message'
# Requires: openai, python-dotenv
#           OPENAI_API_KEY in environment or .env file

import subprocess, sys, os, tempfile
from openai import OpenAI
from dotenv import load_dotenv
from pathlib import Path

here = Path(__file__).resolve().parent
load_dotenv(here / ".env")

client = OpenAI()

sys_prompt = """
You write git commit messages. Follow these rules:
- Always use English
- Subject line: max 50 chars, imperative mood,
  NOT capitalised, no trailing period.
- Blank line between subject and body.
- Body lines wrapped at 72 chars.
- Body should be 3 to 4 lines, in rare cases max. 8 lines
- Explain *what* and *why*, not *how*.
- Use Conventional Commits prefix when clear:
  feat|fix|docs|refactor|test|chore|style|perf
  e.g. "feat: add dark-mode toggle"
Output ONLY the commit message, no commentary.
""".strip()


# run a shell command, return stdout or None
def run(cmd, **args):
    r = subprocess.run(
        cmd, capture_output=True, text=True, **args)
    return r.stdout.strip() if r.returncode == 0 else None


# return (status, diff) or exit on error.
def git_context():
    if run(["git", "rev-parse", "--git-dir"]) is None:
        sys.exit("Not a git repository.")

    status = run(["git", "status", "--short"])
    if not status:
        sys.exit("Nothing to commit.")

    # Prefer staged diff; fall back to full working-tree
    diff = run(["git", "diff", "HEAD"])
    if not diff:
        sys.exit("No diff available.")

    return status, diff


# Call chat completions; return message string.
def ask_openai(status, diff):
    prompt = (
        f"Changed files:\n{status}\n\n"
        f"Diff:\n{diff[:6000]}"   # trim very large diffs
    )
    resp = client.chat.completions.create(
        model="gpt-5.4-mini",
        messages=[
            {"role": "system", "content": sys_prompt},
            {"role": "user",   "content": prompt},
        ],
        temperature=0.3,
    )
    return resp.choices[0].message.content.strip()


# Open $EDITOR with text; return edited result.
def open_editor(text):
    editor = os.environ.get("EDITOR", "nano")
    with tempfile.NamedTemporaryFile(
        suffix=".txt", mode="w", delete=False
    ) as f:
        f.write(text)
        path = f.name
    subprocess.run([editor, path])
    with open(path) as f:
        result = f.read().strip()
    os.unlink(path)
    return result

# stage all changes and commit.
def commit(message):
    result = subprocess.run(
        ["git", "commit", "-a", "-m", message] )
    sys.exit(result.returncode)


# show message, ask accept/edit/abort
def prompt_user(message):
    while True:
        print("\n" + "─" * 50)
        print(message)
        print("─" * 50)
        choice = input(
            "[A]ccept  [E]dit  [Q]uit > "
        ).strip().lower()

        if choice in ("a", ""):
            commit(message)
        elif choice == "e":
            message = open_editor(message)
            if not message:
                print("Empty message – try again.")
        elif choice in ("q", "quit", "abort"):
            sys.exit("Aborted.")

# main code starts here
def main():
    status, diff = git_context()
    print("Generating commit message…")
    message = ask_openai(status, diff)
    prompt_user(message)


if __name__ == "__main__":
    main()
