"""Git automation helpers for ``prompt git`` commands."""

from __future__ import annotations

import subprocess
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path

from dotpromptz.errors import GhCliError, GitError, WorkflowError
from dotpromptz.logging import get_logger

logger = get_logger(__name__)

_COMMIT_MESSAGE_PREFIX = 'chore(prompt): '
_COMMIT_MESSAGE_MAX_LENGTH = 180
_WORKFLOW_FILE_NAME = 'submit-prompt.yml'
_WORKFLOW_RELATIVE_PATH = Path('.github') / 'workflows' / _WORKFLOW_FILE_NAME


@dataclass(frozen=True)
class PushResult:
    """Result for local commit + push automation."""

    committed: bool
    commit_sha: str | None = None
    commit_message: str | None = None


def _extract_process_detail(result: subprocess.CompletedProcess[str]) -> str:
    """Return a concise error detail from process output."""
    detail = (result.stderr or result.stdout).strip()
    return detail


def _run_git(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
    """Run a git command and return the completed process."""
    command = ['git', *args]
    logger.debug('Running git command.', command=' '.join(command), cwd=str(cwd))
    try:
        return subprocess.run(  # noqa: S603
            command,
            cwd=str(cwd),
            check=False,
            text=True,
            capture_output=True,
        )
    except FileNotFoundError as exc:
        raise GitError(
            'git command not found. Please install git first.',
            code='git_missing',
        ) from exc


def _run_gh(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
    """Run a GitHub CLI command and return the completed process."""
    command = ['gh', *args]
    logger.debug('Running gh command.', command=' '.join(command), cwd=str(cwd))
    try:
        return subprocess.run(  # noqa: S603
            command,
            cwd=str(cwd),
            check=False,
            text=True,
            capture_output=True,
        )
    except FileNotFoundError as exc:
        raise GhCliError(
            'GitHub CLI (gh) not found. Please install gh first.',
            code='gh_missing',
        ) from exc


def _raise_git_failure(
    message: str,
    *,
    code: str,
    result: subprocess.CompletedProcess[str],
) -> None:
    """Raise a ``GitError`` with subprocess detail if available."""
    detail = _extract_process_detail(result)
    if detail:
        message = f'{message} {detail}'
    raise GitError(message, code=code)


def _raise_gh_failure(
    message: str,
    *,
    code: str,
    result: subprocess.CompletedProcess[str],
) -> None:
    """Raise a ``GhCliError`` with subprocess detail if available."""
    detail = _extract_process_detail(result)
    if detail:
        message = f'{message} {detail}'
    raise GhCliError(message, code=code)


def resolve_repo_root(cwd: Path | None = None) -> Path:
    """Resolve and return the current git repository root path."""
    working_dir = Path.cwd() if cwd is None else cwd
    result = _run_git(['rev-parse', '--show-toplevel'], cwd=working_dir)
    if result.returncode != 0:
        _raise_git_failure(
            'Current directory is not inside a git repository.',
            code='git_not_repo',
            result=result,
        )

    root = result.stdout.strip()
    if not root:
        raise GitError('Failed to resolve git repository root.', code='git_root_missing')
    return Path(root)


def _resolve_current_branch(repo_root: Path) -> str:
    """Return the current branch name and reject detached HEAD state."""
    result = _run_git(['symbolic-ref', '--quiet', '--short', 'HEAD'], cwd=repo_root)
    if result.returncode != 0:
        _raise_git_failure(
            'Detached HEAD is not supported for prompt git commands.',
            code='git_detached_head',
            result=result,
        )

    branch = result.stdout.strip()
    if not branch:
        raise GitError('Failed to resolve current branch.', code='git_branch_missing')
    return branch


def _ensure_upstream_branch(repo_root: Path) -> None:
    """Ensure current branch has an upstream branch configured."""
    result = _run_git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd=repo_root)
    if result.returncode != 0:
        _raise_git_failure(
            'Current branch has no upstream. Run `git push --set-upstream origin <branch>` first.',
            code='git_upstream_missing',
            result=result,
        )


def sync_repo(cwd: Path | None = None) -> None:
    """Sync local branch with upstream using fast-forward only."""
    repo_root = resolve_repo_root(cwd)
    _resolve_current_branch(repo_root)
    _ensure_upstream_branch(repo_root)

    fetch_result = _run_git(['fetch', '--prune'], cwd=repo_root)
    if fetch_result.returncode != 0:
        _raise_git_failure(
            'Failed to fetch remote updates.',
            code='git_fetch_failed',
            result=fetch_result,
        )

    pull_result = _run_git(['pull', '--ff-only'], cwd=repo_root)
    if pull_result.returncode != 0:
        _raise_git_failure(
            'Cannot fast-forward local branch. Please resolve manually and retry.',
            code='git_sync_ff_only_failed',
            result=pull_result,
        )

    logger.info('Repository synced with upstream (ff-only).', repo_root=str(repo_root))


def _has_staged_changes(repo_root: Path) -> bool:
    """Return whether there are staged changes."""
    result = _run_git(['diff', '--cached', '--quiet'], cwd=repo_root)
    if result.returncode == 0:
        return False
    if result.returncode == 1:
        return True
    _raise_git_failure(
        'Failed to inspect staged changes.',
        code='git_diff_cached_failed',
        result=result,
    )


def _to_change_token(line: str) -> str | None:
    """Convert one ``git diff --name-status`` line to a compact token."""
    if not line.strip():
        return None

    parts = line.split('\t')
    if len(parts) < 2:
        return None

    status_raw = parts[0].strip()
    if not status_raw:
        return None

    status = status_raw[0]
    if status == 'R' and len(parts) >= 3:
        return f'R:{parts[1]}->{parts[2]}'
    return f'{status}:{parts[1]}'


def _truncate_commit_message(message: str) -> str:
    """Truncate commit message by total length only."""
    if len(message) <= _COMMIT_MESSAGE_MAX_LENGTH:
        return message
    return f'{message[: _COMMIT_MESSAGE_MAX_LENGTH - 3]}...'


def build_commit_message_from_staged(repo_root: Path) -> str:
    """Build commit message strictly from staged file changes."""
    result = _run_git(['diff', '--cached', '--name-status'], cwd=repo_root)
    if result.returncode != 0:
        _raise_git_failure(
            'Failed to read staged file changes.',
            code='git_diff_name_status_failed',
            result=result,
        )

    tokens: list[str] = []
    for line in result.stdout.splitlines():
        token = _to_change_token(line)
        if token:
            tokens.append(token)

    if not tokens:
        tokens.append('M:unknown')

    body = ', '.join(tokens)
    message = f'{_COMMIT_MESSAGE_PREFIX}{body}'
    return _truncate_commit_message(message)


def _resolve_head_sha(repo_root: Path) -> str:
    """Return current HEAD commit SHA."""
    result = _run_git(['rev-parse', 'HEAD'], cwd=repo_root)
    if result.returncode != 0:
        _raise_git_failure(
            'Failed to resolve HEAD commit SHA.',
            code='git_head_sha_failed',
            result=result,
        )

    sha = result.stdout.strip()
    if not sha:
        raise GitError('Resolved empty HEAD SHA.', code='git_head_sha_empty')
    return sha


def commit_and_push(cwd: Path | None = None) -> PushResult:
    """Stage all changes, commit with auto message, and push."""
    repo_root = resolve_repo_root(cwd)
    sync_repo(repo_root)

    add_result = _run_git(['add', '.'], cwd=repo_root)
    if add_result.returncode != 0:
        _raise_git_failure(
            'Failed to stage repository changes.',
            code='git_add_failed',
            result=add_result,
        )

    if not _has_staged_changes(repo_root):
        logger.info('No staged changes found; skip commit/push.', repo_root=str(repo_root))
        return PushResult(committed=False, commit_sha=None, commit_message=None)

    commit_message = build_commit_message_from_staged(repo_root)
    commit_result = _run_git(['commit', '-m', commit_message], cwd=repo_root)
    if commit_result.returncode != 0:
        _raise_git_failure(
            'Failed to create git commit.',
            code='git_commit_failed',
            result=commit_result,
        )

    commit_sha = _resolve_head_sha(repo_root)

    push_result = _run_git(['push'], cwd=repo_root)
    if push_result.returncode == 0:
        logger.info('Git push completed.', commit_sha=commit_sha)
        return PushResult(committed=True, commit_sha=commit_sha, commit_message=commit_message)

    logger.warning('Initial push failed; retry once after ff-only sync.')
    fetch_result = _run_git(['fetch', '--prune'], cwd=repo_root)
    if fetch_result.returncode != 0:
        _raise_git_failure(
            'Push failed and fetch for retry also failed.',
            code='git_push_retry_fetch_failed',
            result=fetch_result,
        )

    pull_result = _run_git(['pull', '--ff-only'], cwd=repo_root)
    if pull_result.returncode != 0:
        _raise_git_failure(
            'Push failed and branch cannot be fast-forward synced for retry.',
            code='git_push_retry_ff_only_failed',
            result=pull_result,
        )

    retry_push_result = _run_git(['push'], cwd=repo_root)
    if retry_push_result.returncode != 0:
        _raise_git_failure(
            'Push failed after one ff-only retry.',
            code='git_push_retry_failed',
            result=retry_push_result,
        )

    logger.info('Git push completed after ff-only retry.', commit_sha=commit_sha)
    return PushResult(committed=True, commit_sha=commit_sha, commit_message=commit_message)


def ensure_gh_authenticated(cwd: Path | None = None) -> None:
    """Ensure GitHub CLI is installed and authenticated."""
    repo_root = resolve_repo_root(cwd)

    version_result = _run_gh(['--version'], cwd=repo_root)
    if version_result.returncode != 0:
        _raise_gh_failure(
            'GitHub CLI (gh) is not available.',
            code='gh_version_failed',
            result=version_result,
        )

    auth_result = _run_gh(['auth', 'status'], cwd=repo_root)
    if auth_result.returncode != 0:
        _raise_gh_failure(
            'GitHub CLI is not authenticated. Run `gh auth login` first.',
            code='gh_not_authenticated',
            result=auth_result,
        )


def install_workflow(cwd: Path | None = None) -> Path:
    """Install built-in submit workflow into the current repository."""
    repo_root = resolve_repo_root(cwd)
    target = repo_root / _WORKFLOW_RELATIVE_PATH
    target.parent.mkdir(parents=True, exist_ok=True)

    try:
        template_file = files('dotpromptz.workflow').joinpath(_WORKFLOW_FILE_NAME)
        content = template_file.read_text(encoding='utf-8')
    except FileNotFoundError as exc:
        raise WorkflowError(
            'Built-in submit workflow template not found.',
            code='workflow_template_missing',
        ) from exc

    existing = target.read_text(encoding='utf-8') if target.is_file() else None
    if existing == content:
        logger.info('Workflow template already up to date.', target=str(target))
        return target

    target.write_text(content, encoding='utf-8')
    logger.info('Installed workflow template.', target=str(target))
    return target


def _validate_prompt_path(prompt_file: str, repo_root: Path) -> str:
    """Validate prompt file path and return repo-relative POSIX path."""
    if not prompt_file.strip():
        raise WorkflowError('Prompt file path is required.', code='workflow_prompt_required')

    raw_path = Path(prompt_file)
    full_path = raw_path if raw_path.is_absolute() else repo_root / raw_path
    resolved_path = full_path.resolve()
    resolved_root = repo_root.resolve()

    try:
        relative_path = resolved_path.relative_to(resolved_root)
    except ValueError as exc:
        raise WorkflowError(
            f'Prompt file escapes repository root: {prompt_file}',
            code='workflow_prompt_outside_repo',
        ) from exc

    if not resolved_path.is_file():
        raise WorkflowError(
            f'Prompt file not found: {prompt_file}',
            code='workflow_prompt_not_found',
        )

    relative_str = relative_path.as_posix()
    if relative_str.endswith('.prompt.template'):
        raise WorkflowError(
            f'Prompt template is not runnable: {relative_str}',
            code='workflow_prompt_template_not_allowed',
        )
    if not relative_str.endswith('.prompt'):
        raise WorkflowError(
            f'Prompt file must end with .prompt: {relative_str}',
            code='workflow_prompt_extension_invalid',
        )
    return relative_str


def submit_prompt(
    prompt_file: str,
    *,
    send_mail: bool = True,
    cwd: Path | None = None,
) -> PushResult:
    """Ensure workflow, sync/push repository, and submit GitHub Action job."""
    repo_root = resolve_repo_root(cwd)
    prompt_path = _validate_prompt_path(prompt_file, repo_root)

    ensure_gh_authenticated(repo_root)
    install_workflow(repo_root)
    push_result = commit_and_push(repo_root)
    branch = _resolve_current_branch(repo_root)
    send_mail_value = 'true' if send_mail else 'false'

    run_result = _run_gh(
        [
            'workflow',
            'run',
            _WORKFLOW_FILE_NAME,
            '--ref',
            branch,
            '-f',
            f'prompt_file={prompt_path}',
            '-f',
            f'send_mail={send_mail_value}',
        ],
        cwd=repo_root,
    )
    if run_result.returncode != 0:
        _raise_gh_failure(
            'Failed to trigger submit-prompt workflow.',
            code='gh_workflow_run_failed',
            result=run_result,
        )

    logger.info(
        'submit-prompt workflow dispatched.',
        prompt_file=prompt_path,
        branch=branch,
        send_mail=send_mail,
    )
    return push_result
