#!/usr/bin/env python3
# WL-Pre-Push-Hook-Version: config-driven-git-flow-protection
"""
Pre-push hook: enforces Git Flow branch protection and push policy.

Replaces the previous hardcoded {main, develop} protection with a
config-driven approach:

  - Protected branches come from GitFlowConfig (.wl/git-flow.yaml overrides
    plus the built-in defaults where main/develop have protected: True).
  - If the config cannot be read, protection falls back to the safe defaults
    {main, develop} (protection is never opened up because the config was
    unavailable).
  - Git Flow policy is hard-enforced at push: release/hotfix branches must
    bump VERSION and CHANGELOG.md, branch naming is validated, and direct
    pushes to protected main/develop are blocked (require_pr semantics).

Backend resolution (hybrid):
  1. In-process import of py_wlcommands.commands.git.* when available.
  2. Otherwise fall back to the `wl` CLI via subprocess.
  3. Otherwise fall back to default {main, develop} branch protection only
     (the git-flow policy check is skipped when wl is unavailable).

Behavior:
  - Parses stdin for git pre-push input (one line per ref being pushed).
  - Blocks pushes where the remote ref is a protected branch.
  - Blocks pushes that violate Git Flow policy.
  - Blocks --no-verify / -n flags (prevents bypass attempts).
  - All other refs (feature branches, tags, deletions) are allowed.
  - On stdin read/parse failure, prints a warning and exits 0 (fail-open).
"""

import os
import shutil
import subprocess
import sys

# Set stdout/stderr encoding to UTF-8 for cross-platform emoji support.
# On Windows with Chinese locale (GBK), emoji characters crash print().
if sys.stdout.encoding and sys.stdout.encoding.upper() not in ("UTF-8", "UTF8"):
    _stdout = sys.stdout.buffer if hasattr(sys.stdout, "buffer") else None
    if _stdout:
        sys.stdout = type(sys.stdout)(
            _stdout,
            encoding="utf-8",
            errors="replace",
        )
    _stderr = sys.stderr.buffer if hasattr(sys.stderr, "buffer") else None
    if _stderr:
        sys.stderr = type(sys.stderr)(
            _stderr,
            encoding="utf-8",
            errors="replace",
        )


def check_no_verify(argv: list[str]) -> int | None:
    """Return exit code 1 if --no-verify or -n is present, None otherwise."""
    if "--no-verify" in argv or "-n" in argv:
        print(
            "错误: 不允许使用 --no-verify 或 -n 选项绕过 pre-push 检查。",
            file=sys.stderr,
        )
        print(
            "请使用 feature 分支推送代码，而不是绕过分支保护。",
            file=sys.stderr,
        )
        print(file=sys.stderr)
        print(
            "ERROR: --no-verify/-n is not allowed for protected branches.",
            file=sys.stderr,
        )
        print(
            "Use a feature branch instead of bypassing branch protection.",
            file=sys.stderr,
        )
        return 1
    return None


def read_stdin() -> str:
    """Read all stdin input, handling bytes decoding."""
    try:
        raw = sys.stdin.buffer.read()
        return raw.decode("utf-8", errors="replace")
    except AttributeError:
        # Fallback when sys.stdin.buffer is unavailable
        return sys.stdin.read()


def parse_remote_refs(data: str) -> list[str]:
    """
    Parse git pre-push stdin and extract remote ref names.

    Git pre-push format (one line per ref being pushed):
        <local-ref> <local-sha1> <remote-ref> <remote-sha1>

    Returns list of remote refs like 'refs/heads/main'.
    """
    refs: list[str] = []
    for line in data.splitlines():
        line = line.strip()
        if not line:
            continue

        parts = line.split()
        # Format: local-ref local-sha remote-ref remote-sha
        if len(parts) >= 4:
            remote_ref = parts[2]
            if remote_ref.startswith("refs/heads/"):
                refs.append(remote_ref)
            # Tags (refs/tags/) and other refs are not checked
    return refs


def _resolve_protected_branches() -> frozenset[str]:
    """Resolve the protected branch ref set from GitFlowConfig.

    Protected branches come from the configured main/develop branch names.
    Falls back to the safe default {main, develop} whenever the config cannot
    be read — protection is never opened up because the config was missing,
    malformed, or py_wlcommands could not be imported.
    """
    try:
        from py_wlcommands.commands.git.config import GitFlowConfig

        config = GitFlowConfig()
        return frozenset(
            {
                f"refs/heads/{config.main_branch}",
                f"refs/heads/{config.develop_branch}",
            }
        )
    except Exception:
        return frozenset({"refs/heads/main", "refs/heads/develop"})


def check_branch_protection(
    remote_refs: list[str],
    protected_branches: frozenset[str] | None = None,
) -> int:
    """Check if any remote ref is a protected branch. Returns 1 if blocked."""
    if protected_branches is None:
        protected_branches = _resolve_protected_branches()

    for ref in remote_refs:
        if ref in protected_branches:
            branch = ref.removeprefix("refs/heads/")
            print(f'🚫 PUSH BLOCKED: Direct push to "{branch}" branch is not allowed.')
            print()
            print("📋 Git Flow Policy:")
            print("   - Never push directly to main or develop")
            print("   - Create a feature/fix branch for your changes")
            print("   - Submit changes via Pull Request only")
            print()
            print("🤖 For AI Agents:")
            print("   - Use `git checkout -b feature/<description>` to create a branch")
            print(
                "   - Push to your feature branch instead:"
                " `git push origin feature/<description>`"
            )
            print("   - Then create a Pull Request for review")
            return 1

    return 0


def _load_git_flow_backend() -> dict | str:
    """Load the in-process Git Flow backend.

    Returns:
        dict: backend with config/validator/hook/git_facts when available.
        "subprocess": py_wlcommands is not importable; use the wl CLI.
        "defaults": the config could not be read; skip policy validation.
    """
    try:
        from py_wlcommands.commands.git import git_facts
        from py_wlcommands.commands.git import hook as git_hook
        from py_wlcommands.commands.git.config import GitFlowConfig
        from py_wlcommands.commands.git.policy import GitFlowPolicyValidator

        config = GitFlowConfig()
        return {
            "config": config,
            "validator": GitFlowPolicyValidator(config),
            "hook": git_hook,
            "git_facts": git_facts,
        }
    except ImportError:
        return "subprocess"
    except Exception:
        return "defaults"


def _run_wl_subprocess_check() -> int:
    """Validate the push via the `wl` CLI subprocess.

    Returns 1 when the CLI blocks the push, 0 when allowed or wl is missing.
    """
    wl_exe = shutil.which("wl")
    if not wl_exe:
        return 0
    try:
        result = subprocess.run(
            [wl_exe, "git-flow", "check"],
            capture_output=True,
            text=True,
            check=False,
        )
        if result.stdout:
            print(result.stdout.strip())
        if result.stderr:
            print(result.stderr.strip(), file=sys.stderr)
        return 1 if result.returncode != 0 else 0
    except (OSError, subprocess.SubprocessError):
        return 0


def _run_git_flow_policy(stdin_data: str) -> int:
    """Enforce Git Flow policy for the push. Returns 1 if blocked, else 0."""
    backend = _load_git_flow_backend()
    if backend == "defaults":
        # Policy skipped; branch protection already ran with default {main, develop}.
        return 0
    if backend == "subprocess":
        return _run_wl_subprocess_check()

    config = backend["config"]
    validator = backend["validator"]
    git_hook = backend["hook"]
    git_facts = backend["git_facts"]

    local_branch = git_facts.get_current_branch()
    local_type = validator.classify_branch(local_branch)
    source = (
        config.source_branch(local_type)
        if local_type in {"feature", "release", "hotfix"}
        else None
    )
    changed_files = (
        git_facts.get_changed_files(source, file_filters=["VERSION", "CHANGELOG.md"])
        if source
        else frozenset()
    )
    context = git_hook.build_push_context(
        local_branch=local_branch,
        stdin_text=stdin_data,
        changed_files=changed_files,
    )
    result = validator.validate_push(context)
    if not result.ok:
        print(git_hook.format_hook_failure(result))
        return 1
    return 0


def main() -> int:
    """Main entry point for the pre-push hook."""

    # Step 1: Block --no-verify bypass attempts
    result = check_no_verify(sys.argv)
    if result is not None:
        return result

    # Step 2: Read stdin (fail-open on read errors)
    try:
        data = read_stdin()
    except Exception as exc:
        print(
            f"Warning: Could not read pre-push input: {exc}",
            file=sys.stderr,
        )
        print(
            "Warning: Branch protection check skipped.",
            file=sys.stderr,
        )
        return 0  # fail-open

    # Step 3: Parse remote refs from stdin (fail-open on parse errors)
    try:
        remote_refs = parse_remote_refs(data)
    except Exception as exc:
        print(
            f"Warning: Could not parse pre-push input: {exc}",
            file=sys.stderr,
        )
        print(
            "Warning: Branch protection check skipped.",
            file=sys.stderr,
        )
        return 0  # fail-open

    # Step 4: Branch protection (config-driven with safe default fallback)
    protected = _resolve_protected_branches()
    if check_branch_protection(remote_refs, protected) != 0:
        return 1

    # Step 5: Git Flow policy validation (hybrid backend)
    return _run_git_flow_policy(data)


if __name__ == "__main__":
    sys.exit(main())
