<?php
/**
 * Antara — Auth REST router (single entry point).
 *
 * URL pattern: /core/auth/api.php?action=<name>
 *
 * Legacy gridAuth* actions used by modules/core/storageAdapter.js are handled
 * from includes/api.php (index.php ?action=…) via gridauth_compat.php.
 *
 * All responses are JSON: { success: bool, data?: any, error?: string, code?: int }.
 * POST bodies are JSON. CSRF token must travel in `X-CSRF-Token` header
 * (preferred) or in the JSON body field `csrf` for state-changing endpoints
 * (everything except authLogin / webauthnLoginBegin / webauthnLoginFinish,
 * which are protected by rate limiting and challenge-response respectively).
 * authBootstrapFirstAdmin uses CSRF + the same IP rate bucket pattern as login.
 */

declare(strict_types=1);

require_once __DIR__ . '/bootstrap_auth.php';
require_once __DIR__ . '/profile_image.php';
require_once __DIR__ . '/profile_image_ops.php';
require_once __DIR__ . '/register_user.php';
require_once __DIR__ . '/../security/rate_limit.php';
require_once __DIR__ . '/../security/csrf.php';
require_once __DIR__ . '/../security/sanitize.php';
require_once __DIR__ . '/../security/logger.php';

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate');

require_once dirname(__DIR__, 2) . '/includes/os_traffic_meter.php';
grid_traffic_meter_begin();

function antara_api_send($data, int $status = 200): void
{
    http_response_code($status);
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function antara_api_ok($data = null): void { antara_api_send(['success' => true, 'data' => $data]); }
function antara_api_err(string $msg, int $http = 400, ?string $code = null): void
{
    antara_api_send(['success' => false, 'error' => $msg, 'code' => $code], $http);
}

function antara_method(): string { return strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); }

function antara_require_login(): array
{
    $u = AntaraSession::user();
    if (!$u) antara_api_err('Authentication required.', 401, 'AUTH_REQUIRED');
    return $u;
}

function antara_require_role(array $allowed): array
{
    $u = antara_require_login();
    if (!in_array($u['role'] ?? '', $allowed, true)) {
        antara_api_err('Forbidden.', 403, 'ROLE_DENIED');
    }
    return $u;
}

function antara_public_user(array $u): array
{
    return [
        'id'         => $u['id'] ?? null,
        'email'      => $u['email'] ?? '',
        'username'   => $u['username'] ?? '',
        'phone'      => $u['phone'] ?? '',
        'role'       => $u['role'] ?? 'usuario',
        'active'     => (bool)($u['active'] ?? true),
        'blocked'    => (bool)($u['blocked'] ?? false),
        'createdAt'  => $u['createdAt'] ?? null,
        'lastLoginAt'=> $u['lastLoginAt'] ?? null,
        'hasPasskey' => !empty($u['webauthnCredentialIds']),
        'requirePasswordReset' => !empty($u['requirePasswordReset']),
        'branchId'   => (string)($u['branchId'] ?? ''),
        'departmentId' => (string)($u['departmentId'] ?? ''),
        'positionId' => (string)($u['positionId'] ?? ''),
        'profileImageRel' => antara_user_profile_image_rel($u),
        'profileMediaFrame' => isset($u['profileMediaFrame']) && is_array($u['profileMediaFrame'])
            ? antara_user_profile_media_frame_normalize($u['profileMediaFrame'])
            : null,
        'profileMediaKind' => antara_user_profile_media_kind(antara_user_profile_image_rel($u)),
    ];
}

/**
 * Resuelve branchId/departmentId/positionId para alta o edición de usuario.
 *
 * @param array<string, mixed> $body
 * @param array<string, mixed>|null $existing
 * @return array{branchId: string, departmentId: string, positionId: string, error: string|null}
 */
function antara_user_org_fields_from_body(array $ws, array $body, ?array $existing = null): array
{
    require_once dirname(__DIR__, 2) . '/includes/org_structure_helpers.php';

    $branchId = $existing !== null ? (string) ($existing['branchId'] ?? '') : '';
    if (isset($body['branchId']) && is_string($body['branchId'])) {
        $branchId = trim($body['branchId']);
    } elseif ($branchId === '' && !empty($ws['defaultBranchId'])) {
        $branchId = (string) $ws['defaultBranchId'];
    }

    $departmentId = $existing !== null ? (string) ($existing['departmentId'] ?? '') : '';
    $positionId = $existing !== null ? (string) ($existing['positionId'] ?? '') : '';

    if (array_key_exists('branchId', $body) && !array_key_exists('departmentId', $body) && !array_key_exists('positionId', $body)) {
        $prevBranch = $existing !== null ? (string) ($existing['branchId'] ?? '') : '';
        if ($branchId !== $prevBranch) {
            $departmentId = '';
            $positionId = '';
        }
    }

    if (array_key_exists('departmentId', $body)) {
        $departmentId = trim((string) $body['departmentId']);
    }
    if (array_key_exists('positionId', $body)) {
        $positionId = trim((string) $body['positionId']);
    }

    if (!antara_org_is_company_mode($ws)) {
        return ['branchId' => $branchId, 'departmentId' => '', 'positionId' => '', 'error' => null];
    }

    if ($branchId !== '' && !antara_settings_valid_branch_id($ws, $branchId)) {
        return ['branchId' => $branchId, 'departmentId' => $departmentId, 'positionId' => $positionId, 'error' => 'Sucursal no válida.'];
    }

    $orgErr = antara_org_validate_user_fields($ws, $branchId, $departmentId, $positionId);
    if ($orgErr !== null) {
        return ['branchId' => $branchId, 'departmentId' => $departmentId, 'positionId' => $positionId, 'error' => $orgErr];
    }

    return ['branchId' => $branchId, 'departmentId' => $departmentId, 'positionId' => $positionId, 'error' => null];
}

function antara_public_config(array $cfg): array
{
    // Subset of config that is safe to expose to anonymous clients.
    $loginReq = (bool)$cfg['login_required'];
    return [
        'login_required'          => $loginReq,
        'allow_user_registration' => (bool)$cfg['allow_user_registration'],
        'public_desktop_without_users' => !empty($cfg['public_desktop_without_users']),
        'allow_webauthn'          => (bool)$cfg['allow_webauthn'],
        'allow_password_login'    => (bool)$cfg['allow_password_login'],
        'rp_id'                   => AntaraWebAuthn::rpId($cfg),
        'rp_name'                 => AntaraWebAuthn::rpName($cfg),
        /** True when anonymous desktop is allowed and there are zero accounts (admin first-user wizard). */
        'needs_first_admin_wizard' => !$loginReq && antara_auth_active_user_count() === 0,
    ];
}

/* ============================================================
   Routing
   ============================================================ */

$action = (string)($_GET['action'] ?? '');
$method = antara_method();
$body   = $method === 'POST' ? AntaraSanitize::readJsonBody() : [];

if (AntaraStore::tryConfigRead() === null) {
    antara_api_err(
        'Cofre criptográfico ilegible. Abre la aplicación en el navegador para ver las opciones de recuperación de Flexia.',
        503,
        'VAULT_UNREADABLE'
    );
}

try {
    switch ($action) {

        /* ---------------- CONFIG ---------------- */

        case 'authConfigGet': {
            $cfg = AntaraStore::configRead();
            antara_api_ok([
                'config' => antara_public_config($cfg),
                'csrf'   => AntaraCsrf::token(),
                'user'   => AntaraSession::user() ? antara_public_user(AntaraStore::findUserById(AntaraSession::user()['id']) ?? AntaraSession::user()) : null,
            ]);
            break;
        }

        case 'authConfigSave': {
            antara_require_role([AntaraStore::ROLE_SUPER]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $patch = [];
            foreach (['login_required','allow_user_registration','public_desktop_without_users','allow_webauthn','allow_password_login'] as $k) {
                if (array_key_exists($k, $body)) $patch[$k] = AntaraSanitize::bool($body[$k]);
            }
            if (isset($body['session_timeout_minutes'])) $patch['session_timeout_minutes'] = AntaraSanitize::intRange($body['session_timeout_minutes'], 5, 1440, 60);
            if (isset($body['max_failed_attempts']))     $patch['max_failed_attempts']     = AntaraSanitize::intRange($body['max_failed_attempts'], 1, 100, 5);
            if (isset($body['lockout_minutes']))         $patch['lockout_minutes']         = AntaraSanitize::intRange($body['lockout_minutes'], 1, 1440, 15);
            if (isset($body['rp_name']) && is_string($body['rp_name'])) $patch['rp_name'] = AntaraSanitize::str($body['rp_name'], 64);
            if (array_key_exists('rp_id', $body)) {
                if ($body['rp_id'] === null) {
                    $patch['rp_id'] = null;
                } elseif (is_string($body['rp_id'])) {
                    $rid = AntaraSanitize::rpId($body['rp_id']);
                    if (trim($body['rp_id']) !== '' && $rid === null) {
                        antara_api_err('rp_id no es un dominio u IPv4 válidos.', 400, 'BAD_RP_ID');
                    }
                    $patch['rp_id'] = $rid;
                } else {
                    antara_api_err('rp_id debe ser texto o null.', 400, 'BAD_RP_ID');
                }
            }
            $cfg = AntaraStore::configWrite($patch);
            AntaraLogger::log('config.update', ['by' => AntaraSession::user()['id'], 'patch' => array_keys($patch)]);
            antara_api_ok(['config' => antara_public_config($cfg), 'full' => $cfg]);
            break;
        }

        /* ---------------- AUTH ---------------- */

        case 'authMe': {
            $u = AntaraSession::user();
            if (!$u) antara_api_ok(['user' => null]);
            $fresh = AntaraStore::findUserById($u['id']);
            antara_api_ok([
                'user' => $fresh ? antara_public_user($fresh) : null,
                'csrf' => AntaraCsrf::token(),
            ]);
            break;
        }

        case 'authLogin': {
            if ($method !== 'POST') antara_api_err('Method not allowed.', 405);
            $cfg = AntaraStore::configRead();
            if (!$cfg['allow_password_login']) {
                antara_api_err('El inicio de sesión con contraseña está deshabilitado por el administrador.', 403, 'PWD_DISABLED');
            }

            $login = AntaraSanitize::str($body['username'] ?? '', 254);
            if ($login !== '') {
                if (strpos($login, '@') !== false) {
                    $login = strtolower($login);
                } else {
                    $login = preg_replace('/\s+/', '', $login);
                }
            }
            $pwd   = (string)($body['password'] ?? '');
            if ($login === '' || $pwd === '') {
                antara_api_err('Ingresa tu correo o teléfono y tu contraseña.', 400, 'BAD_INPUT');
            }

            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
            [$locked, $retry] = AntaraRateLimit::checkBeforeAttempt($ip, $login, $cfg);
            if ($locked) {
                AntaraLogger::log('auth.lockout_hit', ['login' => $login]);
                antara_api_err(
                    'Demasiados intentos fallidos. Vuelve a intentarlo en ' . $retry . ' segundos.',
                    429,
                    'LOCKED'
                );
            }

            $user = AntaraStore::findUserByLogin($login);
            $okHash = $user && !empty($user['passwordHash']) && password_verify($pwd, $user['passwordHash']);
            if (!$user || !$okHash) {
                AntaraRateLimit::recordFailure($ip, $login, $cfg);
                AntaraLogger::log('auth.login_fail', ['login' => $login]);
                antara_api_err(
                    'Correo, teléfono o contraseña incorrectos. Revisa tus datos e inténtalo de nuevo.',
                    401,
                    'BAD_CREDENTIALS'
                );
            }
            if (!empty($user['requirePasswordReset'])) {
                AntaraRateLimit::recordFailure($ip, $login, $cfg);
                AntaraLogger::log('auth.login_password_reset_required', ['user' => $user['id']]);
                antara_api_err(
                    'Debes establecer una nueva contraseña (cuenta migrada o marcada por el administrador). Usa el panel admin o el flujo de recuperación.',
                    403,
                    'PASSWORD_RESET_REQUIRED'
                );
            }
            if (!empty($user['blocked'])) {
                AntaraLogger::log('auth.login_blocked', ['user' => $user['id']]);
                antara_api_err('Tu cuenta está bloqueada. Contacta al administrador.', 403, 'BLOCKED');
            }
            if (empty($user['active'])) {
                antara_api_err('Tu cuenta está inactiva. Contacta al administrador.', 403, 'INACTIVE');
            }

            AntaraRateLimit::recordSuccess($ip, $login, $user['id']);
            AntaraSession::login($user);
            AntaraCsrf::rotate();
            AntaraLogger::log('auth.login_ok', ['user' => $user['id'], 'method' => 'password']);
            antara_api_ok([
                'user' => antara_public_user(AntaraStore::findUserById($user['id'])),
                'csrf' => AntaraCsrf::token(),
            ]);
            break;
        }

        case 'authLogout': {
            $u = AntaraSession::user();
            AntaraSession::destroy();
            if ($u) AntaraLogger::log('auth.logout', ['user' => $u['id']]);
            antara_api_ok(['user' => null]);
            break;
        }

        case 'authRegister': {
            if ($method !== 'POST') antara_api_err('Method not allowed.', 405);
            $cfg = AntaraStore::configRead();
            if (!$cfg['allow_user_registration']) {
                antara_api_err('La creación de nuevos usuarios está deshabilitada por el administrador.', 403, 'SIGNUP_DISABLED');
            }
            $countBefore = antara_auth_active_user_count();
            try {
                require_once dirname(__DIR__, 2) . '/includes/settings_helpers.php';
                $ws = antara_workspace_read_settings();
                $regBody = $body;
                $bid = trim((string)($ws['defaultBranchId'] ?? ''));
                if ($bid !== '' && antara_settings_valid_branch_id($ws, $bid)) {
                    $regBody['branchId'] = $bid;
                }
                $user = antara_auth_register_user($regBody, false);
            } catch (RuntimeException $e) {
                $msg = $e->getMessage();
                if (str_contains($msg, 'deshabilitada')) {
                    antara_api_err($msg, 403, 'SIGNUP_DISABLED');
                }
                if (str_contains($msg, 'correo') || str_contains($msg, 'usuario')) {
                    antara_api_err($msg, 409, 'DUPLICATE');
                }
                antara_api_err($msg, 400, 'BAD_INPUT');
            }
            if ($countBefore === 0) {
                antara_auth_privatize_public_desktop();
            }

            AntaraSession::login($user);
            AntaraCsrf::rotate();
            AntaraLogger::log('auth.register', ['user' => $user['id'], 'role' => $user['role'] ?? '']);
            antara_api_ok([
                'user' => antara_public_user($user),
                'csrf' => AntaraCsrf::token(),
            ]);
            break;
        }

        case 'authBootstrapFirstAdmin': {
            if ($method !== 'POST') antara_api_err('Method not allowed.', 405);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $cfg = AntaraStore::configRead();
            $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '');
            $bootstrapLogin = '__bootstrap_first_admin__';
            [$locked, $retry] = AntaraRateLimit::checkBeforeAttempt($ip, $bootstrapLogin, $cfg);
            if ($locked) {
                AntaraLogger::log('auth.bootstrap_first_lockout', ['ip' => $ip]);
                antara_api_err('Demasiados intentos. Vuelve a intentarlo en ' . $retry . ' s.', 429, 'LOCKED');
            }
            if (!empty($cfg['login_required'])) {
                AntaraRateLimit::recordFailure($ip, $bootstrapLogin, $cfg);
                antara_api_err('El escritorio ya exige inicio de sesión.', 403, 'BOOTSTRAP_DENIED');
            }
            if (antara_auth_active_user_count() !== 0) {
                AntaraRateLimit::recordFailure($ip, $bootstrapLogin, $cfg);
                antara_api_err('Ya existen usuarios en el sistema.', 409, 'BOOTSTRAP_NOT_EMPTY');
            }
            try {
                require_once dirname(__DIR__, 2) . '/includes/settings_helpers.php';
                $ws = antara_workspace_read_settings();
                $bid = (string)($ws['defaultBranchId'] ?? '');
                $reg = $body;
                if ($bid !== '' && antara_settings_valid_branch_id($ws, $bid)) {
                    $reg['branchId'] = $bid;
                }
                $user = antara_auth_register_user($reg, true);
            } catch (RuntimeException $e) {
                AntaraRateLimit::recordFailure($ip, $bootstrapLogin, $cfg);
                $msg = $e->getMessage();
                if (str_contains($msg, 'correo') || str_contains($msg, 'usuario')) {
                    antara_api_err($msg, 409, 'DUPLICATE');
                }
                antara_api_err($msg, 400, 'BAD_INPUT');
            }
            antara_auth_privatize_public_desktop();
            AntaraRateLimit::recordSuccess($ip, $bootstrapLogin, $user['id']);
            AntaraSession::login($user);
            AntaraCsrf::rotate();
            AntaraLogger::log('auth.bootstrap_first_admin_ok', ['user' => $user['id']]);
            $cfgOut = AntaraStore::configRead();
            antara_api_ok([
                'user'   => antara_public_user(AntaraStore::findUserById($user['id']) ?? $user),
                'config' => antara_public_config($cfgOut),
                'csrf'   => AntaraCsrf::token(),
            ]);
            break;
        }

        /* ---------------- WEBAUTHN ---------------- */

        case 'webauthnRegisterBegin': {
            $cfg = AntaraStore::configRead();
            if (!$cfg['allow_webauthn']) antara_api_err('WebAuthn deshabilitado.', 403, 'WEBAUTHN_DISABLED');
            $u = antara_require_login();
            AntaraCsrf::assertFromHeaderOrBody($body);
            $fresh = AntaraStore::findUserById($u['id']);
            if (!$fresh) antara_api_err('User not found.', 404);
            $opts = AntaraWebAuthn::registerBegin($fresh, $cfg);
            antara_api_ok($opts);
            break;
        }

        case 'webauthnRegisterFinish': {
            $cfg = AntaraStore::configRead();
            if (!$cfg['allow_webauthn']) antara_api_err('WebAuthn deshabilitado.', 403, 'WEBAUTHN_DISABLED');
            $u = antara_require_login();
            AntaraCsrf::assertFromHeaderOrBody($body);
            $cred = $body['credential'] ?? null;
            if (!is_array($cred)) antara_api_err('Missing credential.', 400);
            $res = AntaraWebAuthn::registerFinish($cred, $cfg);
            AntaraLogger::log('webauthn.register_ok', ['user' => $u['id'], 'cred' => $res['credentialId']]);
            antara_api_ok($res);
            break;
        }

        case 'webauthnLoginBegin': {
            $cfg = AntaraStore::configRead();
            if (!$cfg['allow_webauthn']) antara_api_err('WebAuthn deshabilitado.', 403, 'WEBAUTHN_DISABLED');
            $login = isset($body['login']) ? AntaraSanitize::str($body['login'], 254) : null;
            // If a login was provided and the user has no credentials, return code so
            // the UI can decide between wizard vs error message.
            if ($login !== null && $login !== '') {
                $u = AntaraStore::findUserByLogin($login);
                if (!$u || empty(AntaraStore::credsForUser($u['id']))) {
                    antara_api_ok([
                        'noCredentials' => true,
                        'allow_user_registration' => (bool)$cfg['allow_user_registration'],
                    ]);
                    break;
                }
            }
            $opts = AntaraWebAuthn::loginBegin($cfg, $login ?: null);
            antara_api_ok($opts);
            break;
        }

        case 'webauthnLoginFinish': {
            $cfg = AntaraStore::configRead();
            if (!$cfg['allow_webauthn']) antara_api_err('WebAuthn deshabilitado.', 403, 'WEBAUTHN_DISABLED');
            $cred = $body['credential'] ?? null;
            if (!is_array($cred)) antara_api_err('Missing credential.', 400);
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
            try {
                $res = AntaraWebAuthn::loginFinish($cred, $cfg);
            } catch (\Throwable $e) {
                AntaraRateLimit::recordFailure($ip, 'webauthn:' . ($cred['id'] ?? ''), $cfg);
                AntaraLogger::log('webauthn.login_fail', ['error' => $e->getMessage()]);
                antara_api_err('Verificación biométrica fallida.', 401, 'WEBAUTHN_FAIL');
            }
            $user = $res['user'];
            if (!empty($user['blocked'])) antara_api_err('User is blocked.', 403, 'BLOCKED');
            if (empty($user['active'])) antara_api_err('User is inactive.', 403, 'INACTIVE');
            AntaraRateLimit::recordSuccess($ip, 'webauthn:' . $res['credential']['credentialId'], $user['id']);
            AntaraSession::login($user);
            AntaraCsrf::rotate();
            AntaraLogger::log('auth.login_ok', ['user' => $user['id'], 'method' => 'webauthn']);
            antara_api_ok([
                'user' => antara_public_user(AntaraStore::findUserById($user['id'])),
                'csrf' => AntaraCsrf::token(),
            ]);
            break;
        }

        /* ---------------- USERS (admin) ---------------- */

        case 'usersList': {
            antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            $blob = AntaraStore::usersAll();
            $list = [];
            foreach ($blob['users'] as $u) {
                if (!empty($u['deletedAt'])) continue;
                $list[] = antara_public_user($u);
            }
            antara_api_ok(['users' => $list]);
            break;
        }

        case 'usersCreate': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $email = AntaraSanitize::email($body['email'] ?? '');
            $username = AntaraSanitize::username($body['username'] ?? '');
            $phone = AntaraSanitize::phone($body['phone'] ?? '') ?? '';
            $password = AntaraSanitize::password($body['password'] ?? '');
            $role = AntaraSanitize::role($body['role'] ?? AntaraStore::ROLE_USER) ?? AntaraStore::ROLE_USER;
            if ($role === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                antara_api_err('Only super_admin can create super_admin.', 403);
            }
            if (!$email && !$username) antara_api_err('Email or username required.', 400);
            if (!$password) antara_api_err('Password too weak.', 400);
            if ($email && AntaraStore::findUserByLogin($email)) antara_api_err('Email already exists.', 409);
            if ($username && AntaraStore::findUserByLogin($username)) antara_api_err('Username already exists.', 409);

            require_once dirname(__DIR__, 2) . '/includes/settings_helpers.php';
            $ws = antara_workspace_read_settings();
            $org = antara_user_org_fields_from_body($ws, $body);
            if ($org['error'] !== null) {
                antara_api_err($org['error'], 400, 'BAD_ORG_FIELDS');
            }

            $u = AntaraStore::upsertUser([
                'email'    => (string)($email ?: ''),
                'username' => (string)($username ?: ''),
                'phone'    => $phone,
                'role'     => $role,
                'active'   => isset($body['active']) ? AntaraSanitize::bool($body['active']) : true,
                'passwordHash' => password_hash($password, PASSWORD_DEFAULT),
                'requirePasswordReset' => false,
                'branchId' => $org['branchId'],
                'departmentId' => $org['departmentId'],
                'positionId' => $org['positionId'],
            ]);
            AntaraLogger::log('users.create', ['by' => $actor['id'], 'user' => $u['id']]);
            antara_api_ok(['user' => antara_public_user($u)]);
            break;
        }

        case 'usersUpdate': {
            $actor = antara_require_login();
            AntaraCsrf::assertFromHeaderOrBody($body);
            $id = AntaraSanitize::str($body['id'] ?? '');
            if (!$id) antara_api_err('Missing user id.', 400);
            $target = AntaraStore::findUserById($id);
            if (!$target) antara_api_err('User not found.', 404);
            $isSelf = ($actor['id'] ?? '') === $id;
            $isAdmin = in_array($actor['role'] ?? '', [AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN], true);
            if (!$isAdmin && !$isSelf) {
                antara_api_err('Forbidden.', 403, 'ROLE_DENIED');
            }
            $patch = ['id' => $id];
            if (isset($body['email'])) {
                if (!$isAdmin) {
                    antara_api_err('Cannot modify email.', 403);
                }
                $email = AntaraSanitize::email($body['email']);
                if ($email === null && $body['email'] !== '') antara_api_err('Bad email.', 400);
                $patch['email'] = $email ?: '';
            }
            if (isset($body['username'])) {
                $u = AntaraSanitize::username($body['username']);
                if ($u === null && $body['username'] !== '') antara_api_err('Bad username.', 400);
                $patch['username'] = $u ?: '';
            }
            if (isset($body['phone'])) {
                if (!$isAdmin) {
                    antara_api_err('Cannot modify phone.', 403);
                }
                $p = AntaraSanitize::phone($body['phone']);
                $patch['phone'] = $p ?? '';
            }
            if (isset($body['role'])) {
                if (!$isAdmin) {
                    antara_api_err('Cannot modify role.', 403);
                }
                $role = AntaraSanitize::role($body['role']);
                if (!$role) antara_api_err('Bad role.', 400);
                if ($role === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                    antara_api_err('Only super_admin can assign super_admin.', 403);
                }
                if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                    antara_api_err('Cannot modify a super_admin.', 403);
                }
                $patch['role'] = $role;
            }
            if (array_key_exists('branchId', $body) || array_key_exists('departmentId', $body) || array_key_exists('positionId', $body)) {
                if (!$isAdmin) {
                    antara_api_err('Cannot modify organization fields.', 403);
                }
                require_once dirname(__DIR__, 2) . '/includes/settings_helpers.php';
                $ws = antara_workspace_read_settings();
                $org = antara_user_org_fields_from_body($ws, $body, $target);
                if ($org['error'] !== null) {
                    antara_api_err($org['error'], 400, 'BAD_ORG_FIELDS');
                }
                $patch['branchId'] = $org['branchId'];
                $patch['departmentId'] = $org['departmentId'];
                $patch['positionId'] = $org['positionId'];
            }
            if (array_key_exists('clearProfileImage', $body) && AntaraSanitize::bool($body['clearProfileImage'] ?? false)) {
                if (!$isSelf && !$isAdmin) {
                    antara_api_err('Forbidden.', 403);
                }
                if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                    antara_api_err('Cannot modify a super_admin.', 403);
                }
                antara_user_profile_image_delete_uploaded_files($id);
                $patch['profileImageRel'] = '';
                $patch['profileMediaFrame'] = null;
            }
            $u = AntaraStore::upsertUser($patch, $id);
            AntaraLogger::log('users.update', ['by' => $actor['id'], 'user' => $id, 'fields' => array_keys($patch)]);
            antara_api_ok(['user' => antara_public_user($u)]);
            break;
        }

        case 'usersProfileImage': {
            if ($method !== 'POST') {
                antara_api_err('Method not allowed.', 405);
            }
            $ct = strtolower((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
            if (strpos($ct, 'multipart/form-data') === false) {
                antara_api_err('Usa multipart/form-data con el archivo.', 400, 'BAD_CONTENT');
            }
            $actor = antara_require_login();
            AntaraCsrf::assertFromHeaderOrBody([]);
            $userId = AntaraSanitize::str((string) ($_POST['userId'] ?? ''), 40);
            if (!antara_user_profile_is_uuid($userId)) {
                antara_api_err('Usuario no válido.', 400, 'BAD_USER');
            }
            $target = AntaraStore::findUserById($userId);
            if (!$target) {
                antara_api_err('User not found.', 404);
            }
            $isSelf = ($actor['id'] ?? '') === $userId;
            $isAdmin = in_array($actor['role'] ?? '', [AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN], true);
            if (!$isSelf && !$isAdmin) {
                antara_api_err('Forbidden.', 403, 'ROLE_DENIED');
            }
            if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                antara_api_err('Cannot modify a super_admin.', 403);
            }
            $file = $_FILES['file'] ?? null;
            if (!is_array($file)) {
                antara_api_err('Falta el archivo (campo file).', 400, 'NO_FILE');
            }
            [$err, $rel] = antara_user_profile_image_process_upload($userId, $file);
            if ($err !== null) {
                antara_api_err($err, 400, 'UPLOAD_FAIL');
            }
            $patch = ['id' => $userId, 'profileImageRel' => $rel];
            $kind = antara_user_profile_media_kind($rel);
            $frameRaw = $_POST['mediaFrame'] ?? '';
            if (is_string($frameRaw) && $frameRaw !== '') {
                $decoded = json_decode($frameRaw, true);
                $frame = antara_user_profile_media_frame_normalize($decoded);
                if ($frame !== null && in_array($kind, ['gif', 'video'], true)) {
                    $patch['profileMediaFrame'] = $frame;
                }
            } elseif ($kind === 'image') {
                $patch['profileMediaFrame'] = null;
            }
            $u = AntaraStore::upsertUser($patch, $userId);
            AntaraLogger::log('users.profile_image', ['by' => $actor['id'], 'user' => $userId]);
            antara_api_ok(['user' => antara_public_user($u)]);
            break;
        }

        case 'clientLogoUpload': {
            if ($method !== 'POST') {
                antara_api_err('Method not allowed.', 405);
            }
            $ct = strtolower((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
            if (strpos($ct, 'multipart/form-data') === false) {
                antara_api_err('Usa multipart/form-data con el archivo.', 400, 'BAD_CONTENT');
            }
            $actor = antara_require_role([AntaraStore::ROLE_SUPER]);
            AntaraCsrf::assertFromHeaderOrBody([]);
            require_once dirname(__DIR__, 2) . '/includes/client_logo_ops.php';
            require_once dirname(__DIR__, 2) . '/includes/grid_default_settings.php';
            require_once dirname(__DIR__, 2) . '/includes/settings_helpers.php';
            $file = $_FILES['file'] ?? null;
            if (!is_array($file)) {
                antara_api_err('Falta el archivo (campo file).', 400, 'NO_FILE');
            }
            $source = isset($_POST['source']) ? (string) $_POST['source'] : 'original';
            $frame = antara_media_frame_from_request_value($_POST['mediaFrame'] ?? '');
            $result = antara_client_logo_process_upload($file, $source, $frame);
            if ($result['errors'] !== []) {
                antara_api_err($result['errors'][0], 400, 'UPLOAD_FAIL');
            }
            if (!$result['saved']) {
                antara_api_err('No se recibió ningún archivo.', 400, 'NO_FILE');
            }
            $root = dirname(__DIR__, 2);
            $sf = $root . DIRECTORY_SEPARATOR . 'settings.json';
            $bk = $root . DIRECTORY_SEPARATOR . 'settings.json.bak';
            if (!antara_client_logo_sync_settings($sf, $bk, grid_app_default_settings(), $result['mediaFrame'])) {
                antara_api_err('No se pudo guardar la configuración del logo.', 500, 'SAVE_FAILED');
            }
            $settings = grid_read_settings_for_api($sf, $bk, grid_app_default_settings());
            AntaraLogger::log('settings.client_logo', ['by' => $actor['id']]);
            antara_api_ok([
                'clientLogoRel' => (string) ($settings['clientLogoRel'] ?? ''),
                'clientLogoMediaFrame' => $settings['clientLogoMediaFrame'] ?? null,
                'clientLogoKind' => antara_user_profile_media_kind('/' . (string) ($settings['clientLogoRel'] ?? '')),
            ]);
            break;
        }

        case 'clientLogoClear': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            require_once dirname(__DIR__, 2) . '/includes/client_logo_ops.php';
            require_once dirname(__DIR__, 2) . '/includes/grid_default_settings.php';
            require_once dirname(__DIR__, 2) . '/includes/settings_helpers.php';
            $dir = antara_sinapsys_logos_dir();
            foreach (glob($dir . DIRECTORY_SEPARATOR . 'logo_cliente.*') ?: [] as $old) {
                if (is_file($old)) {
                    @unlink($old);
                }
            }
            $root = dirname(__DIR__, 2);
            $sf = $root . DIRECTORY_SEPARATOR . 'settings.json';
            $bk = $root . DIRECTORY_SEPARATOR . 'settings.json.bak';
            if (!antara_client_logo_sync_settings($sf, $bk, grid_app_default_settings(), null)) {
                antara_api_err('No se pudo actualizar la configuración.', 500, 'SAVE_FAILED');
            }
            AntaraLogger::log('settings.client_logo_clear', ['by' => $actor['id']]);
            antara_api_ok(['clientLogoRel' => '', 'clientLogoMediaFrame' => null]);
            break;
        }

        case 'usersToggleActive': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $id = AntaraSanitize::str($body['id'] ?? '');
            $active = AntaraSanitize::bool($body['active'] ?? true);
            $target = AntaraStore::findUserById($id);
            if (!$target) antara_api_err('User not found.', 404);
            if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                antara_api_err('Cannot modify a super_admin.', 403);
            }
            $u = AntaraStore::upsertUser(['id' => $id, 'active' => $active], $id);
            AntaraLogger::log('users.toggle_active', ['by' => $actor['id'], 'user' => $id, 'active' => $active]);
            antara_api_ok(['user' => antara_public_user($u)]);
            break;
        }

        case 'usersBlock': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $id = AntaraSanitize::str($body['id'] ?? '');
            $blocked = AntaraSanitize::bool($body['blocked'] ?? true);
            $target = AntaraStore::findUserById($id);
            if (!$target) antara_api_err('User not found.', 404);
            if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                antara_api_err('Cannot modify a super_admin.', 403);
            }
            $u = AntaraStore::upsertUser(['id' => $id, 'blocked' => $blocked], $id);
            AntaraLogger::log('users.block', ['by' => $actor['id'], 'user' => $id, 'blocked' => $blocked]);
            antara_api_ok(['user' => antara_public_user($u)]);
            break;
        }

        case 'usersDelete': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $id = AntaraSanitize::str($body['id'] ?? '');
            $target = AntaraStore::findUserById($id);
            if (!$target) antara_api_err('User not found.', 404);
            if ($id === ($actor['id'] ?? '')) antara_api_err('Cannot delete yourself.', 400);
            if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                antara_api_err('Cannot delete a super_admin.', 403);
            }
            AntaraStore::softDeleteUser($id);
            AntaraStore::deleteCredentialsForUser($id);
            AntaraLogger::log('users.delete', ['by' => $actor['id'], 'user' => $id]);
            antara_api_ok(['ok' => true]);
            break;
        }

        case 'usersResetPasskey': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            $id = AntaraSanitize::str($body['id'] ?? '');
            $target = AntaraStore::findUserById($id);
            if (!$target) antara_api_err('User not found.', 404);
            if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                antara_api_err('Cannot modify a super_admin.', 403);
            }
            AntaraStore::deleteCredentialsForUser($id);
            AntaraLogger::log('users.reset_passkey', ['by' => $actor['id'], 'user' => $id]);
            antara_api_ok(['ok' => true]);
            break;
        }

        case 'usersChangePassword': {
            $actor = antara_require_login();
            AntaraCsrf::assertFromHeaderOrBody($body);
            $id = AntaraSanitize::str($body['id'] ?? $actor['id']);
            $newPwd = AntaraSanitize::password($body['password'] ?? '');
            if (!$newPwd) antara_api_err('Password too weak (min 8 chars).', 400);

            // Self-change requires the current password (unless requirePasswordReset).
            if ($id === $actor['id'] && empty($actor['requirePasswordReset'])) {
                $current = (string)($body['current'] ?? '');
                $fresh = AntaraStore::findUserById($id);
                if (!$fresh || !password_verify($current, $fresh['passwordHash'] ?? '')) {
                    antara_api_err('Contraseña actual incorrecta.', 401);
                }
            } else {
                if (!in_array($actor['role'], [AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN], true)) {
                    antara_api_err('Forbidden.', 403);
                }
                $target = AntaraStore::findUserById($id);
                if (!$target) antara_api_err('User not found.', 404);
                if (($target['role'] ?? '') === AntaraStore::ROLE_SUPER && $actor['role'] !== AntaraStore::ROLE_SUPER) {
                    antara_api_err('Cannot modify a super_admin.', 403);
                }
            }
            AntaraStore::upsertUser([
                'id' => $id,
                'passwordHash' => password_hash($newPwd, PASSWORD_DEFAULT),
                'requirePasswordReset' => false,
            ], $id);
            AntaraLogger::log('users.change_password', ['by' => $actor['id'], 'user' => $id]);
            antara_api_ok(['ok' => true]);
            break;
        }

        /* ---------------- ORG STRUCTURE (company mode) ---------------- */

        case 'orgStructureGet': {
            antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            require_once dirname(__DIR__, 2) . '/includes/org_structure_helpers.php';
            $ws = antara_workspace_read_settings();
            if (!antara_org_is_company_mode($ws)) {
                antara_api_err('Solo disponible en modo empresa u organización.', 400, 'NOT_COMPANY_MODE');
            }
            antara_api_ok([
                'branches' => antara_org_public_branches($ws['branches'] ?? []),
                'deploymentKind' => (string) ($ws['deploymentKind'] ?? 'company'),
            ]);
            break;
        }

        case 'orgStructureSave': {
            $actor = antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            AntaraCsrf::assertFromHeaderOrBody($body);
            require_once dirname(__DIR__, 2) . '/includes/org_structure_helpers.php';
            $ws = antara_workspace_read_settings();
            if (!antara_org_is_company_mode($ws)) {
                antara_api_err('Solo disponible en modo empresa u organización.', 400, 'NOT_COMPANY_MODE');
            }
            $branchId = AntaraSanitize::str($body['branchId'] ?? '', 80);
            if ($branchId === '') {
                antara_api_err('Indica la sucursal.', 400, 'MISSING_BRANCH');
            }
            $departments = $body['departments'] ?? [];
            $result = antara_org_apply_branch_departments($ws, $branchId, is_array($departments) ? $departments : []);
            if ($result['error'] !== null) {
                antara_api_err($result['error'], 400, 'BAD_ORG_STRUCTURE');
            }
            if (!antara_workspace_persist_settings($result['settings'])) {
                antara_api_err('No se pudo guardar la estructura organizacional.', 500, 'PERSIST_FAILED');
            }
            AntaraLogger::log('org.structure_save', ['by' => $actor['id'], 'branchId' => $branchId]);
            antara_api_ok([
                'branches' => antara_org_public_branches($result['settings']['branches'] ?? []),
            ]);
            break;
        }

        /* ---------------- LOGS ---------------- */

        case 'securityLogTail': {
            antara_require_role([AntaraStore::ROLE_SUPER, AntaraStore::ROLE_ADMIN]);
            $n = AntaraSanitize::intRange($_GET['n'] ?? 200, 1, 1000, 200);
            antara_api_ok(['lines' => AntaraLogger::tail($n)]);
            break;
        }

        default:
            antara_api_err('Unknown action: ' . $action, 404, 'UNKNOWN_ACTION');
    }
} catch (\Throwable $e) {
    AntaraLogger::log('api.exception', ['action' => $action, 'error' => $e->getMessage()]);
    antara_api_err('Internal error.', 500, 'INTERNAL');
}
