"""Shared, dependency-free helpers for BIG EGG's local development commands."""
from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time
from datetime import datetime, timezone

# Assez pour couvrir un envoi Drive (quelques centaines de millisecondes),
# assez peu pour qu'une vraie permission refusee se signale vite.
REPLACE_ATTEMPTS = 6
PROJECT = Path(__file__).resolve().parents[2]
DEV = PROJECT / 'tools/dev'
RUNS = DEV / 'runs'
SKIP = {'.git', '.godot', '_backups', 'node_modules', '__pycache__', 'runs', 'exports'}


def now():
    return datetime.now(timezone.utc).isoformat(timespec='seconds')


def digest(data):
    return hashlib.sha256(data).hexdigest()


def read_json(path):
    return json.loads(Path(path).read_text(encoding='utf-8-sig'))


def atomic_write(path, data, expected=None):
    """Replace a whole file; reject a stale read before replacing its contents."""
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    if expected is not None and digest(path.read_bytes()) != expected:
        raise ValueError(f'File changed since read: {path}')
    fd, temporary = tempfile.mkstemp(prefix=path.name + '.', suffix='.tmp', dir=path.parent)
    try:
        with os.fdopen(fd, 'wb') as stream:
            stream.write(data)
            stream.flush()
            os.fsync(stream.fileno())
        if expected is not None and digest(path.read_bytes()) != expected:
            raise ValueError(f'File changed while preparing edit: {path}')
        # Sur un dossier synchronise (Google Drive) ou surveille par un
        # antivirus, ce renommage echoue par intermittence avec WinError 5: le
        # destinataire est ouvert ailleurs, le temps d'un envoi. Ce n'est pas un
        # conflit d'ecriture, et abandonner tuait des batteries entieres apres
        # vingt minutes de calcul — quatre fois en une seule session. On laisse
        # passer l'orage sans rien relacher: l'ecriture reste atomique, seule la
        # derniere etape est reessayee.
        for attempt in range(REPLACE_ATTEMPTS):
            try:
                os.replace(temporary, path)
                break
            except PermissionError:
                if attempt == REPLACE_ATTEMPTS - 1:
                    raise
                time.sleep(0.2 * (attempt + 1))
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)


def write_json(path, value):
    atomic_write(path, (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8'))


def project_path(value, must_exist=True):
    path = (PROJECT / value).resolve()
    if not path.is_relative_to(PROJECT) or path == PROJECT:
        raise ValueError('Expected a file inside big-egg-3d')
    if any(part in SKIP for part in path.relative_to(PROJECT).parts):
        raise ValueError('Backups, caches and repository internals are not editable targets')
    if must_exist and not path.is_file():
        raise ValueError(f'File not found: {value}')
    return path


def executable(name):
    overrides = read_json(PROJECT / '.dev-local.json') if (PROJECT / '.dev-local.json').exists() else {}
    explicit = os.environ.get('BIGEGG_' + name.upper()) or overrides.get(name)
    if explicit:
        if Path(explicit).is_file():
            return str(Path(explicit).resolve())
        raise ValueError(f'Invalid {name} executable override')
    candidates = {
        'godot': [Path.home() / 'Downloads/Godot_v4.7.1-stable_win64.exe/Godot_v4.7.1-stable_win64_console.exe'],
        'php': [Path('C:/wamp64/bin/php/php8.4.0/php.exe')],
        'php_cgi': [Path('C:/wamp64/bin/php/php8.4.0/php-cgi.exe')],
        'node': [Path('C:/Program Files/nodejs/node.exe')],
        'blender': [Path('C:/Program Files/Blender Foundation/Blender 5.2/blender.exe')],
    }
    for candidate in candidates.get(name, []):
        if candidate.is_file():
            return str(candidate)
    found = shutil.which({'php_cgi': 'php-cgi'}.get(name, name))
    if found:
        return found
    raise ValueError(f'{name} missing; set BIGEGG_{name.upper()} or .dev-local.json')


def git(*args, check=True):
    result = subprocess.run([executable('git'), '-C', str(PROJECT), *args],
                            capture_output=True, encoding='utf-8', errors='replace')
    if check and result.returncode:
        raise ValueError(result.stderr.strip() or 'Git command failed')
    return result


def source_state():
    """Conservative input inventory; exclude generated caches and art working files."""
    result = {}
    for directory in ['assets', 'autoload', 'data', 'scenes', 'scripts', 'addons',
                      'tools/game-data-manager', 'tools/shapes-3d-manager', 'tools/dev']:
        for parent, dirs, files in os.walk(PROJECT / directory):
            dirs[:] = sorted(d for d in dirs if d not in SKIP)
            for name in sorted(files):
                path = Path(parent) / name
                if path.suffix in {'.log', '.tmp', '.pyc'} or path.name.startswith('.env'):
                    continue
                result[path.relative_to(PROJECT).as_posix()] = digest(path.read_bytes())
    for path in [PROJECT / 'project.godot', PROJECT / 'AGENTS.md', PROJECT / 'CLAUDE.md',
                 PROJECT.parent / 'CLAUDE.md', PROJECT.parent / 'AGENTS.md']:
        if path.is_file():
            result[os.path.relpath(path, PROJECT).replace('\\', '/')] = digest(path.read_bytes())
    return result


def state_digest(state):
    return digest(json.dumps(state, sort_keys=True).encode())


def changed_files(before, after):
    return sorted(key for key in before.keys() | after.keys() if before.get(key) != after.get(key))


def new_run(label):
    RUNS.mkdir(parents=True, exist_ok=True)
    return Path(tempfile.mkdtemp(prefix=datetime.now().strftime('%Y%m%d-%H%M%S-') + label + '-', dir=RUNS))


def isolated_environment(run):
    # Environment overrides affect only our child, never the editor or caller.
    environment = os.environ.copy()
    paths = {'APPDATA': 'appdata', 'LOCALAPPDATA': 'local', 'XDG_DATA_HOME': 'xdg-data',
             'XDG_CONFIG_HOME': 'xdg-config', 'XDG_CACHE_HOME': 'xdg-cache'}
    for key, name in paths.items():
        target = Path(run) / name
        target.mkdir(parents=True, exist_ok=True)
        environment[key] = str(target.resolve())
    environment['BIGEGG_DEV_RUN_DIR'] = str(Path(run).resolve())
    environment['NODE_PATH'] = str(DEV / 'node_modules') + os.pathsep + environment.get('NODE_PATH', '')
    environment['PYTHONDONTWRITEBYTECODE'] = '1'
    try:
        environment['PHP_CGI_BIN'] = executable('php_cgi')
    except ValueError:
        pass
    return environment
