#!/usr/bin/env bash
# Requires Bash 3.2+, Python 3.8+, and curl. No sudo; run one installer at a time.
set -euo pipefail

# Defining the complete function before calling it also avoids starting an
# installation from a download truncated inside the embedded Python program.
main() {
    command -v python3 >/dev/null || { printf '%s\n' 'Python 3.8+ is required.' >&2; return 1; }
    command -v curl >/dev/null || { printf '%s\n' 'curl is required.' >&2; return 1; }
    python3 - <<'PY'
import os
import re
import stat
import subprocess
import sys
import tempfile
from pathlib import Path

BEGIN = b'<!-- BEGIN MATT-POCOCK-GRILLING-QUESTION-TOOLS -->'
END = b'<!-- END MATT-POCOCK-GRILLING-QUESTION-TOOLS -->'
TOKEN = b'MATT-POCOCK-GRILLING-QUESTION-TOOLS'
POLICY_BEGIN = '<!-- BEGIN MATT-POCOCK-GRILLING-QUESTION-POLICY v1 -->'
POLICY_END = '<!-- END MATT-POCOCK-GRILLING-QUESTION-POLICY v1 -->'
BASE = 'https://opengist.resetrix.work/weehong/9e381dc89dff4670b7232b910454f056'


def check_path(path):
    for part in [path] + list(path.parents):
        if part.is_symlink():
            raise ValueError('Symlink refused: %s. Use physical directories and a regular instruction file; manage linked files manually.' % part)
        if part.exists() and not (part.is_file() if part == path else part.is_dir()):
            raise ValueError('Not a regular file/directory: %s' % part)
    parent = path.parent
    while not parent.exists():
        parent = parent.parent
    if not os.access(parent, os.W_OK | os.X_OK):
        raise ValueError('Directory is not writable/searchable: %s' % parent)
    if path.exists() and not os.access(path, os.R_OK | os.W_OK):
        raise ValueError('Instruction file is not readable/writable: %s' % path)


def install():
    if sys.version_info < (3, 8):
        raise ValueError('Python 3.8+ is required.')
    revision = os.environ.get('QUESTION_TOOLS_REVISION') or 'HEAD'
    if revision != 'HEAD' and not re.fullmatch(r'[0-9a-fA-F]{40}', revision):
        raise ValueError('QUESTION_TOOLS_REVISION must be HEAD or a full 40-character commit hash.')
    url = BASE + '/raw/' + revision + '/grilling-question-tools.md'
    result = subprocess.run(['curl', '-fsSL', '--proto', '=https', '--proto-redir', '=https',
                             '--connect-timeout', '15', '--max-time', '60', url],
                            stdout=subprocess.PIPE, check=True)
    policy = result.stdout.decode('utf-8')
    lines = policy.splitlines()
    if (not lines or lines[0] != POLICY_BEGIN or lines[-1] != POLICY_END
            or policy.count(POLICY_BEGIN) != 1 or policy.count(POLICY_END) != 1
            or len(policy) > 32768 or '\x00' in policy
            or TOKEN.decode() in policy
            or not policy.startswith(POLICY_BEGIN + '\n# Question Tools for Matt Pocock Grilling Flows\n')
            or not all('\n%d. ' % i in policy for i in range(1, 8))):
        raise ValueError('Downloaded policy is incomplete or invalid; no instruction files changed.')
    block = BEGIN + b'\n' + policy.rstrip('\r\n').encode('utf-8') + b'\n' + END
    home = os.environ.get('HOME')
    if not home or not os.path.isabs(home):
        raise ValueError('HOME must be an absolute path.')
    directories = [
        os.environ.get('CLAUDE_CONFIG_DIR') or home + '/.claude',
        os.environ.get('CODEX_HOME') or home + '/.codex',
        os.environ.get('OPENCODE_CONFIG_DIR') or
        (os.environ.get('XDG_CONFIG_HOME') or home + '/.config') + '/opencode',
    ]
    if any(not os.path.isabs(d) or '..' in Path(d).parts for d in directories):
        raise ValueError('Config directories must be absolute paths without .. components.')
    paths = list(dict.fromkeys(Path(d) / name for d, name in
                              zip(directories, ['CLAUDE.md', 'AGENTS.md', 'AGENTS.md'])))
    changes = []
    # Preflight every destination before creating directories, backups, or files.
    for path in paths:
        check_path(path)
        exists = path.exists()
        old = path.read_bytes() if exists else b''
        if TOKEN in old:
            if old.count(TOKEN) != 2 or old.count(BEGIN) != 1 or old.count(END) != 1:
                raise ValueError('Malformed/duplicate managed markers: %s. Repair them manually before rerunning.' % path)
            start, end = old.index(BEGIN), old.index(END)
            after = end + len(END)
            if (end < start or (start and old[start-1:start] != b'\n')
                    or old[end-1:end] != b'\n'
                    or (old[after:] and not old[after:].startswith((b'\n', b'\r\n')))
                    or not old[start+len(BEGIN):].startswith((b'\n', b'\r\n'))):
                raise ValueError('Malformed managed marker boundaries: %s' % path)
            new = old[:start] + block + old[after:]
        else:
            separator = (b'\n' if old.endswith(b'\n') else b'\n\n') if old else b''
            new = old + separator + block + b'\n'
        if new != old:
            changes.append((path, exists, old, new))
        else:
            print('Unchanged: %s' % path)

    staged = []
    try:
        for path, exists, old, new in changes:
            check_path(path)
            if path.exists() != exists or (exists and path.read_bytes() != old):
                raise ValueError('File changed during installation: %s. Retry with other writers stopped.' % path)
            path.parent.mkdir(parents=True, exist_ok=True)
            mode = stat.S_IMODE(path.stat().st_mode) if exists else 0o600
            fd, temporary = tempfile.mkstemp(prefix='.' + path.name + '.question-tools-', dir=str(path.parent))
            staged.append((path, temporary))
            with os.fdopen(fd, 'wb') as stream:
                stream.write(new)
                stream.flush()
                os.fsync(stream.fileno())
                os.fchmod(stream.fileno(), mode)
            if exists:
                fd, backup = tempfile.mkstemp(prefix=path.name + '.question-tools-backup-', dir=str(path.parent))
                with os.fdopen(fd, 'wb') as stream:
                    stream.write(old)
                    stream.flush()
                    os.fsync(stream.fileno())
                print('Backup: %s' % backup)
        for path, temporary in staged:
            os.replace(temporary, path)
            print('Updated: %s' % path)
    finally:
        for _, temporary in staged:
            if os.path.exists(temporary):
                os.unlink(temporary)
    print('Policy source: %s' % url)
    print('Start fresh Claude Code/Codex sessions; quit and restart OpenCode.')


try:
    install()
except (OSError, ValueError, subprocess.SubprocessError) as error:
    print('Installation failed: %s' % error, file=sys.stderr)
    sys.exit(1)
PY
}

main "$@"
