<?php
declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';

final class SiapaPeerException extends RuntimeException {
    public function __construct(string $message, public int $httpStatus=500, public array $details=[]) {
        parent::__construct($message);
    }
}

function peer_token_from_request(): string {
    $auth = (string)($_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '');
    if ($auth === '' && function_exists('getallheaders')) {
        $headers = getallheaders();
        $auth = (string)($headers['Authorization'] ?? $headers['authorization'] ?? '');
    }
    if (preg_match('/^Bearer\s+(.+)$/i', trim($auth), $m)) return trim($m[1]);
    return trim((string)($_SERVER['HTTP_X_SIAPA_PEER_TOKEN'] ?? ''));
}

function require_peer_auth(array $config): void {
    $expected = trim((string)($config['peer_token'] ?? ''));
    if ($expected === '' || !hash_equals($expected, peer_token_from_request())) {
        respond(['error'=>'Enlace de réplica no autorizado.'], 401);
    }
    $allowed = array_values(array_filter(array_map('trim', (array)($config['peer_allowed_ips'] ?? []))));
    if ($allowed && !in_array((string)($_SERVER['REMOTE_ADDR'] ?? ''), $allowed, true)) {
        respond(['error'=>'Dirección no autorizada para réplica.'], 403);
    }
}

function peer_validate_payload(mixed $payload): array {
    if (!is_array($payload) || !isset($payload['db']) || !is_array($payload['db']) ||
        !isset($payload['db']['estudiantes'], $payload['db']['docentes']) ||
        !is_array($payload['db']['estudiantes']) || !is_array($payload['db']['docentes'])) {
        throw new SiapaPeerException('La réplica no contiene una estructura SIAPA válida.', 422);
    }
    return $payload;
}

function peer_store_payload(array $config, array $payload, string $deviceId, ?int $expectedRevision=null): array {
    $payload = peer_validate_payload($payload);
    [$dir,$file,$lockFile,$hist] = storage_paths($config);
    $lock = fopen($lockFile, 'c+');
    if (!$lock || !flock($lock, LOCK_EX)) throw new SiapaPeerException('No se pudo bloquear el almacenamiento local.', 503);
    try {
        $current = read_state($file);
        $revision = (int)$current['revision'];
        if ($expectedRevision !== null && $expectedRevision !== $revision) {
            throw new SiapaPeerException('Conflicto de revisión de réplica.', 409, [
                'server_revision'=>$revision,
                'base_revision'=>$expectedRevision,
            ]);
        }
        if ($revision > 0 && is_array($current['payload'])) {
            @file_put_contents(
                $hist.'/revision-'.str_pad((string)$revision,8,'0',STR_PAD_LEFT).'-'.date('Ymd-His').'.json',
                json_encode($current, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
                LOCK_EX
            );
        }
        $next = $revision + 1;
        $state = [
            'revision'=>$next,
            'checksum'=>checksum_payload($payload),
            'updated_at'=>gmdate('c'),
            'device_id'=>substr($deviceId,0,160),
            'payload'=>$payload,
        ];
        $tmp = $file.'.tmp-'.bin2hex(random_bytes(5));
        $json = json_encode($state, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        if (file_put_contents($tmp, $json, LOCK_EX) === false || !rename($tmp, $file)) {
            @unlink($tmp);
            throw new SiapaPeerException('No se pudo escribir la réplica local.', 500);
        }
        prune_history($hist, (int)($config['history_limit'] ?? 25));
        return $state;
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

function peer_http_json(string $method, string $url, string $token, ?array $body=null, int $timeout=25): array {
    $headers = ['Accept: application/json', 'Authorization: Bearer '.$token];
    $encoded = null;
    if ($body !== null) {
        $encoded = json_encode($body, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        $headers[] = 'Content-Type: application/json';
    }
    $status = 0; $raw = false;
    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER=>true,
            CURLOPT_FOLLOWLOCATION=>false,
            CURLOPT_CONNECTTIMEOUT=>min(10,$timeout),
            CURLOPT_TIMEOUT=>$timeout,
            CURLOPT_CUSTOMREQUEST=>$method,
            CURLOPT_HTTPHEADER=>$headers,
            CURLOPT_SSL_VERIFYPEER=>true,
            CURLOPT_SSL_VERIFYHOST=>2,
        ]);
        if ($encoded !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
        $raw = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        if ($raw === false) throw new SiapaPeerException('No se pudo contactar el servidor del dominio: '.$error, 503);
    } else {
        $opts = ['http'=>[
            'method'=>$method,
            'header'=>implode("\r\n",$headers)."\r\n",
            'content'=>$encoded ?? '',
            'timeout'=>$timeout,
            'ignore_errors'=>true,
        ], 'ssl'=>['verify_peer'=>true,'verify_peer_name'=>true]];
        $raw = @file_get_contents($url, false, stream_context_create($opts));
        foreach (($http_response_header ?? []) as $h) if (preg_match('~^HTTP/\S+\s+(\d+)~',$h,$m)) {$status=(int)$m[1];break;}
        if ($raw === false && $status === 0) throw new SiapaPeerException('No se pudo contactar el servidor del dominio.', 503);
    }
    $data = json_decode((string)$raw, true);
    if (!is_array($data)) throw new SiapaPeerException('El servidor del dominio devolvió una respuesta no válida.', 502, ['status'=>$status]);
    if ($status < 200 || $status >= 300) {
        throw new SiapaPeerException((string)($data['error'] ?? ('Error HTTP '.$status)), $status ?: 502, $data);
    }
    return $data;
}

function peer_meta_paths(array $config): array {
    [$dir,,,$hist] = storage_paths($config);
    return [$dir.'/peer-sync-meta.json', $dir.'/peer-sync.log', $hist];
}
function peer_read_meta(string $file): array {
    $d = is_file($file) ? json_decode((string)file_get_contents($file), true) : [];
    return is_array($d) ? $d : [];
}
function peer_write_meta(string $file, array $meta): void {
    $meta['updated_at'] = gmdate('c');
    file_put_contents($file, json_encode($meta, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), LOCK_EX);
}
function peer_log(string $file, string $message): void {
    @file_put_contents($file, '['.gmdate('c').'] '.$message.PHP_EOL, FILE_APPEND|LOCK_EX);
    if (is_file($file) && filesize($file) > 2*1024*1024) {
        @rename($file, $file.'.1');
    }
}
function peer_remote_url(array $config, string $action): string {
    $base = rtrim(trim((string)($config['peer_base_url'] ?? '')), '/');
    if ($base === '') throw new SiapaPeerException('La URL del servidor del dominio no está configurada.', 503);
    if (!str_starts_with($base, 'https://') && empty($config['peer_allow_http'])) {
        throw new SiapaPeerException('La réplica remota exige HTTPS. Para laboratorio local habilite peer_allow_http.', 503);
    }
    return $base.'/peer.php?action='.rawurlencode($action);
}

function peer_sync_once(array $config): array {
    if (empty($config['peer_sync_enabled'])) throw new SiapaPeerException('La réplica servidor-local ↔ dominio está desactivada.', 503);
    $token = trim((string)($config['peer_token'] ?? ''));
    if ($token === '') throw new SiapaPeerException('Falta configurar el token privado de réplica.', 503);
    [$dir,$localFile] = [storage_paths($config)[0], storage_paths($config)[1]];
    [$metaFile,$logFile,$hist] = peer_meta_paths($config);
    $meta = peer_read_meta($metaFile);
    $local = read_state($localFile);
    $timeout = max(5,(int)($config['peer_timeout_seconds'] ?? 25));
    $remote = peer_http_json('GET', peer_remote_url($config,'status'), $token, null, $timeout);
    $localExists = (int)$local['revision'] > 0 && is_array($local['payload']);
    $remoteExists = !empty($remote['exists']) && (int)($remote['revision'] ?? 0) > 0;

    $finish = function(string $action, array $localState, array $remoteState, array $extra=[]) use (&$meta,$metaFile,$logFile): array {
        $meta = array_merge($meta, [
            'last_action'=>$action,
            'last_sync_at'=>gmdate('c'),
            'last_local_checksum'=>$localState['checksum'] ?? null,
            'last_remote_checksum'=>$remoteState['checksum'] ?? null,
            'remote_revision'=>(int)($remoteState['revision'] ?? 0),
            'conflict'=>false,
            'last_error'=>null,
        ], $extra);
        peer_write_meta($metaFile,$meta); peer_log($logFile,$action);
        return ['ok'=>true,'action'=>$action,'local_revision'=>(int)($localState['revision']??0),'remote_revision'=>(int)($remoteState['revision']??0),'checksum'=>$localState['checksum']??$remoteState['checksum']??null];
    };

    if (!$localExists && !$remoteExists) return $finish('SIN_DATOS', $local, $remote);
    if ($localExists && !$remoteExists) {
        $pushed = peer_http_json('POST', peer_remote_url($config,'push'), $token, [
            'base_revision'=>0,'device_id'=>(string)($config['peer_id'] ?? 'SIAPA-LOCAL'),'payload'=>$local['payload']
        ], $timeout);
        $remote = array_merge($remote,$pushed,['exists'=>true,'checksum'=>$local['checksum']]);
        return $finish('LOCAL_A_DOMINIO', $local, $remote);
    }
    if (!$localExists && $remoteExists) {
        $pulled = peer_http_json('GET', peer_remote_url($config,'pull'), $token, null, $timeout);
        $local = peer_store_payload($config, peer_validate_payload($pulled['payload'] ?? null), 'PEER-DOMINIO', null);
        return $finish('DOMINIO_A_LOCAL', $local, $pulled);
    }

    $localChecksum = (string)($local['checksum'] ?? '');
    $remoteChecksum = (string)($remote['checksum'] ?? '');
    if ($localChecksum !== '' && hash_equals($localChecksum,$remoteChecksum)) return $finish('YA_SINCRONIZADO', $local, $remote);

    $lastL = (string)($meta['last_local_checksum'] ?? '');
    $lastR = (string)($meta['last_remote_checksum'] ?? '');
    if ($lastL === '' || $lastR === '') {
        $policy = (string)($config['peer_initial_policy'] ?? 'conflict');
        if ($policy === 'prefer_local') {
            $pushed = peer_http_json('POST', peer_remote_url($config,'push'), $token, ['base_revision'=>(int)$remote['revision'],'device_id'=>(string)($config['peer_id'] ?? 'SIAPA-LOCAL'),'payload'=>$local['payload']], $timeout);
            $remote = array_merge($remote,$pushed,['checksum'=>$localChecksum]);
            return $finish('INICIAL_LOCAL_A_DOMINIO',$local,$remote);
        }
        if ($policy === 'prefer_web' || $policy === 'prefer_cloud') {
            $pulled = peer_http_json('GET', peer_remote_url($config,'pull'), $token, null, $timeout);
            $local = peer_store_payload($config, peer_validate_payload($pulled['payload'] ?? null), 'PEER-DOMINIO', null);
            return $finish('INICIAL_DOMINIO_A_LOCAL',$local,$pulled);
        }
        throw new SiapaPeerException('Conflicto inicial: el servidor local y el dominio contienen copias diferentes.',409,['local_checksum'=>$localChecksum,'remote_checksum'=>$remoteChecksum]);
    }

    $localChanged = !hash_equals($lastL,$localChecksum);
    $remoteChanged = !hash_equals($lastR,$remoteChecksum);
    if ($localChanged && !$remoteChanged) {
        $pushed = peer_http_json('POST', peer_remote_url($config,'push'), $token, ['base_revision'=>(int)$remote['revision'],'device_id'=>(string)($config['peer_id'] ?? 'SIAPA-LOCAL'),'payload'=>$local['payload']], $timeout);
        $remote = array_merge($remote,$pushed,['checksum'=>$localChecksum]);
        return $finish('LOCAL_A_DOMINIO',$local,$remote);
    }
    if ($remoteChanged && !$localChanged) {
        $pulled = peer_http_json('GET', peer_remote_url($config,'pull'), $token, null, $timeout);
        $local = peer_store_payload($config, peer_validate_payload($pulled['payload'] ?? null), 'PEER-DOMINIO', null);
        return $finish('DOMINIO_A_LOCAL',$local,$pulled);
    }

    $pulled = peer_http_json('GET', peer_remote_url($config,'pull'), $token, null, $timeout);
    $conflictFile = $hist.'/peer-conflict-dominio-'.date('Ymd-His').'.json';
    @file_put_contents($conflictFile,json_encode($pulled,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),LOCK_EX);
    $meta = array_merge($meta,['last_sync_at'=>gmdate('c'),'last_action'=>'CONFLICTO','conflict'=>true,'last_error'=>'Cambios simultáneos en local y dominio.','remote_revision'=>(int)($remote['revision']??0)]);
    peer_write_meta($metaFile,$meta); peer_log($logFile,'CONFLICTO: cambios simultáneos; copia remota guardada en '.basename($conflictFile));
    throw new SiapaPeerException('Conflicto de réplica: hubo cambios simultáneos en local y dominio. No se sobrescribió ninguna copia.',409,['conflict_backup'=>basename($conflictFile)]);
}
