<?php
declare(strict_types=1);

const APP_ROOT = __DIR__;
const STORAGE_DIR = APP_ROOT . '/storage';
const CONFIG_FILE = STORAGE_DIR . '/config.php';
const DB_FILE = STORAGE_DIR . '/database.sqlite';
const INSTALL_LOCK = STORAGE_DIR . '/installed.lock';

function app_installed(): bool { return is_file(CONFIG_FILE) && is_file(INSTALL_LOCK); }
function config(): array {
    static $cfg;
    if ($cfg !== null) return $cfg;
    if (!is_file(CONFIG_FILE)) return [];
    $loaded = require CONFIG_FILE;
    if (!is_array($loaded)) throw new RuntimeException('Configuración inválida.');
    return $cfg = $loaded;
}
function db(): PDO {
    static $pdo;
    if ($pdo instanceof PDO) return $pdo;
    $c = config();
    $driver = (string)($c['db_driver'] ?? 'sqlite');
    $options = [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ];
    if ($driver === 'mysql') {
        $dsn = 'mysql:host='.(string)$c['db_host'].';dbname='.(string)$c['db_name'].';charset=utf8mb4';
        $pdo = new PDO($dsn, (string)$c['db_username'], (string)$c['db_password'], $options);
    } else {
        $pdo = new PDO('sqlite:' . DB_FILE, null, null, $options);
        $pdo->exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
    }
    return $pdo;
}
function boot_session(): void {
    if (session_status() === PHP_SESSION_ACTIVE) return;
    $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
    session_name('UEN_CORREO_DOCENTE');
    session_set_cookie_params(['lifetime'=>0,'path'=>'/','secure'=>$secure,'httponly'=>true,'samesite'=>'Strict']);
    session_start();
    if (empty($_SESSION['session_started'])) { session_regenerate_id(true); $_SESSION['session_started']=time(); }
}
function h(string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function redirect(string $url): never { header('Location: '.$url); exit; }
function csrf_token(): string { boot_session(); return $_SESSION['csrf'] ??= bin2hex(random_bytes(32)); }
function verify_csrf(): void {
    boot_session();
    $sent=(string)($_POST['csrf']??'');
    if (!hash_equals($_SESSION['csrf']??'', $sent)) { http_response_code(419); exit('Solicitud expirada o no válida.'); }
}
function normalize_cedula(string $v): string {
    $v=strtoupper(trim($v)); $v=preg_replace('/[^0-9]/','',$v)??'';
    if (strlen($v)<5 || strlen($v)>12) throw new InvalidArgumentException('Número de cédula no válido.');
    return $v;
}
function cedula_hash(string $cedula): string { return hash_hmac('sha256', normalize_cedula($cedula), (string)config()['cedula_pepper']); }
function normalize_email(string $v): string {
    $v=strtolower(trim($v));
    if (!filter_var($v,FILTER_VALIDATE_EMAIL)) throw new InvalidArgumentException('Correo personal no válido.');
    return $v;
}
function app_key(): string { $raw=base64_decode((string)config()['app_key'],true); if($raw===false||strlen($raw)<32) throw new RuntimeException('Clave de aplicación inválida.'); return substr($raw,0,32); }
function encrypt_value(string $plain): string {
    $iv=random_bytes(12); $tag='';
    $cipher=openssl_encrypt($plain,'aes-256-gcm',app_key(),OPENSSL_RAW_DATA,$iv,$tag,'uen-correo');
    if($cipher===false) throw new RuntimeException('No se pudo cifrar el dato.');
    return base64_encode($iv.$tag.$cipher);
}
function decrypt_value(?string $encoded): string {
    if(!$encoded) return '';
    $raw=base64_decode($encoded,true); if($raw===false||strlen($raw)<29) return '';
    $iv=substr($raw,0,12); $tag=substr($raw,12,16); $cipher=substr($raw,28);
    $plain=openssl_decrypt($cipher,'aes-256-gcm',app_key(),OPENSSL_RAW_DATA,$iv,$tag,'uen-correo');
    return $plain===false?'':$plain;
}
function client_ip_hash(): string {
    $ip=(string)($_SERVER['REMOTE_ADDR']??'unknown');
    return hash_hmac('sha256',$ip,(string)config()['rate_pepper']);
}
function rate_limit(string $action, int $max, int $window): bool {
    $key=$action.':'.client_ip_hash(); $now=time(); $pdo=db();
    $pdo->prepare('DELETE FROM rate_limits WHERE window_start < ?')->execute([$now-$window*2]);
    $st=$pdo->prepare('SELECT attempts,window_start FROM rate_limits WHERE rate_key=?'); $st->execute([$key]); $row=$st->fetch();
    if(!$row){$pdo->prepare('INSERT INTO rate_limits(rate_key,attempts,window_start) VALUES(?,?,?)')->execute([$key,1,$now]); return true;}
    if($now-(int)$row['window_start']>$window){$pdo->prepare('UPDATE rate_limits SET attempts=1,window_start=? WHERE rate_key=?')->execute([$now,$key]);return true;}
    if((int)$row['attempts'] >= $max) return false;
    $pdo->prepare('UPDATE rate_limits SET attempts=attempts+1 WHERE rate_key=?')->execute([$key]); return true;
}
function audit(string $event, ?int $teacherId=null, array $meta=[]): void {
    $st=db()->prepare('INSERT INTO audit_log(event,teacher_id,ip_hash,metadata,created_at) VALUES(?,?,?,?,?)');
    $st->execute([$event,$teacherId,client_ip_hash(),json_encode($meta,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),date('c')]);
}
function teacher_by_identity(string $cedula, string $email): ?array {
    $st=db()->prepare('SELECT * FROM teachers WHERE cedula_hash=? AND status<>? LIMIT 1');
    $st->execute([cedula_hash($cedula),'disabled']); $row=$st->fetch();
    if(!$row) return null;
    $registered=decrypt_value($row['personal_email_enc']);
    return hash_equals($registered,normalize_email($email))?$row:null;
}
function teacher_by_id(int $id): ?array { $st=db()->prepare('SELECT * FROM teachers WHERE id=?');$st->execute([$id]);$r=$st->fetch();return $r?:null; }
function generate_otp(int $teacherId, string $purpose='registration'): string {
    $code=(string)random_int(100000,999999); $hash=hash_hmac('sha256',$code,(string)config()['otp_pepper']);
    db()->prepare('UPDATE otp_codes SET used_at=? WHERE teacher_id=? AND purpose=? AND used_at IS NULL')->execute([date('c'),$teacherId,$purpose]);
    db()->prepare('INSERT INTO otp_codes(teacher_id,purpose,code_hash,expires_at,attempts,created_at) VALUES(?,?,?,?,0,?)')->execute([$teacherId,$purpose,$hash,date('c',time()+600),date('c')]);
    return $code;
}
function verify_otp(int $teacherId,string $code,string $purpose='registration'): bool {
    $st=db()->prepare('SELECT * FROM otp_codes WHERE teacher_id=? AND purpose=? AND used_at IS NULL ORDER BY id DESC LIMIT 1');$st->execute([$teacherId,$purpose]);$row=$st->fetch();
    if(!$row||strtotime($row['expires_at'])<time()||(int)$row['attempts']>=5) return false;
    db()->prepare('UPDATE otp_codes SET attempts=attempts+1 WHERE id=?')->execute([$row['id']]);
    $hash=hash_hmac('sha256',preg_replace('/\D/','',$code)??'',(string)config()['otp_pepper']);
    if(!hash_equals($row['code_hash'],$hash)) return false;
    db()->prepare('UPDATE otp_codes SET used_at=? WHERE id=?')->execute([date('c'),$row['id']]); return true;
}
function send_mail_message(string $to,string $subject,string $body): bool {
    $cfg=config(); $from=(string)($cfg['mail_from']??'');
    $headers=['MIME-Version: 1.0','Content-Type: text/plain; charset=UTF-8'];
    if($from) $headers[]='From: U.E.N. Alberto Smith <'.$from.'>';
    return @mail($to,'=?UTF-8?B?'.base64_encode($subject).'?=',$body,implode("\r\n",$headers));
}
function send_registration_otp(array $teacher,string $code): bool {
    $to=decrypt_value($teacher['personal_email_enc']);
    $body="U.E.N. Alberto Smith\n\nCódigo de verificación para registrar su correo institucional: {$code}\n\nVence en 10 minutos. No comparta este código. Si usted no inició este proceso, ignore el mensaje.\n";
    return send_mail_message($to,'Código de verificación del correo institucional',$body);
}
function base32_encode_raw(string $data): string {
    $alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';$bits='';$out='';
    foreach(str_split($data) as $c)$bits.=str_pad(decbin(ord($c)),8,'0',STR_PAD_LEFT);
    foreach(str_split($bits,5) as $chunk){$chunk=str_pad($chunk,5,'0');$out.=$alphabet[bindec($chunk)];}
    return $out;
}
function base32_decode_raw(string $s): string {
    $alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';$s=strtoupper(preg_replace('/[^A-Z2-7]/','',$s)??'');$bits='';$out='';
    foreach(str_split($s) as $c){$p=strpos($alphabet,$c);if($p===false)continue;$bits.=str_pad(decbin($p),5,'0',STR_PAD_LEFT);}
    foreach(str_split($bits,8) as $chunk)if(strlen($chunk)===8)$out.=chr(bindec($chunk));
    return $out;
}
function generate_totp_secret(): string { return base32_encode_raw(random_bytes(20)); }
function totp_code(string $secret,?int $time=null): string {
    $counter=intdiv($time??time(),30);$bin=pack('N*',0).pack('N*',$counter);$hash=hash_hmac('sha1',$bin,base32_decode_raw($secret),true);$offset=ord($hash[19])&0xf;
    $n=((ord($hash[$offset])&0x7f)<<24)|((ord($hash[$offset+1])&0xff)<<16)|((ord($hash[$offset+2])&0xff)<<8)|(ord($hash[$offset+3])&0xff);
    return str_pad((string)($n%1000000),6,'0',STR_PAD_LEFT);
}
function verify_totp(string $secret,string $code): bool { $code=preg_replace('/\D/','',$code)??'';if(strlen($code)!==6)return false;for($i=-1;$i<=1;$i++)if(hash_equals(totp_code($secret,time()+$i*30),$code))return true;return false; }
function valid_username(string $u): bool { return (bool)preg_match('/^[a-z][a-z0-9._-]{2,29}$/',$u); }
function valid_password(string $p): bool { return strlen($p)>=12&&preg_match('/[A-Z]/',$p)&&preg_match('/[a-z]/',$p)&&preg_match('/\d/',$p)&&preg_match('/[^A-Za-z0-9]/',$p); }
function cpanel_configured(): bool { $c=config();return !empty($c['cpanel_host'])&&!empty($c['cpanel_username'])&&!empty($c['cpanel_api_token'])&&function_exists('curl_init'); }
function cpanel_uapi(string $module,string $function,array $params): array {
    $c=config(); if(!cpanel_configured()) return ['ok'=>false,'message'=>'La API de cPanel todavía no está configurada.'];
    $base=rtrim((string)$c['cpanel_host'],'/'); if(!preg_match('#:\d+$#',$base))$base.=':2083';
    $url=$base.'/execute/'.rawurlencode($module).'/'.rawurlencode($function).'?'.http_build_query($params,'','&',PHP_QUERY_RFC3986);
    $ch=curl_init($url);curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>25,CURLOPT_CONNECTTIMEOUT=>10,CURLOPT_HTTPHEADER=>['Authorization: cpanel '.$c['cpanel_username'].':'.$c['cpanel_api_token'],'Accept: application/json'],CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2]);
    $body=curl_exec($ch);$err=curl_error($ch);$status=(int)curl_getinfo($ch,CURLINFO_RESPONSE_CODE);curl_close($ch);
    if($body===false||$err)return ['ok'=>false,'message'=>'No fue posible conectar con cPanel.'];
    $json=json_decode($body,true);$result=$json['result']??null;
    if($status>=200&&$status<300&&is_array($result)&&((int)($result['status']??0)===1))return ['ok'=>true,'data'=>$result['data']??null];
    $errors=is_array($result['errors']??null)?implode(' ',array_map('strval',$result['errors'])):(string)($result['errors']??'Error de cPanel');
    return ['ok'=>false,'message'=>$errors?:'No se pudo crear la cuenta.'];
}
function create_cpanel_mailbox(string $username,string $password): array {
    $c=config();return cpanel_uapi('Email','add_pop',['email'=>$username,'password'=>$password,'domain'=>$c['domain'],'quota'=>(string)$c['mail_quota_mb'],'send_welcome_email'=>'0']);
}
function admin_logged_in(): bool { boot_session();return !empty($_SESSION['admin_id']); }
function require_admin(): void { if(!admin_logged_in()) redirect('admin.php'); }
function flash(string $type,string $message): void { boot_session();$_SESSION['flash'][]=['type'=>$type,'message'=>$message]; }
function pull_flashes(): array { boot_session();$f=$_SESSION['flash']??[];unset($_SESSION['flash']);return $f; }
function mask_email(string $email): string { [$a,$d]=array_pad(explode('@',$email,2),2,'');return substr($a,0,2).str_repeat('•',max(2,strlen($a)-2)).'@'.$d; }
?>
