<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Auth;
use App\Core\Database;
use App\Core\Request;
use App\Core\Response;
use PDO;
use Throwable;

final class FunctionsController
{
    public function lookupMunicipios(Request $request): void
    {
        $body = $this->body($request);
        $identifier = strtolower(trim((string)(
            $body['usuario']
            ?? $body['username']
            ?? $body['login']
            ?? $body['email']
            ?? $request->query['usuario']
            ?? $request->query['username']
            ?? $request->query['login']
            ?? $request->query['email']
            ?? ''
        )));

        if (!$identifier) {
            Response::json(['municipios' => [], 'data' => [], 'isSuper' => false]);
            return;
        }

        // E-mail continua único globalmente; username é resolvido por vínculo municipal.
        $user = str_contains($identifier, '@')
            ? Database::fetchOne('SELECT * FROM users WHERE lower(email)=lower(:login) LIMIT 1', ['login' => $identifier])
            : $this->findGlobalUserByUsername($identifier);

        if ($user) {
            $roles = array_column(Database::fetchAll('SELECT role FROM user_roles WHERE user_id=:id', ['id' => $user['id']]), 'role');
            $isSuper = filter_var($user['is_superuser'] ?? false, FILTER_VALIDATE_BOOL) || in_array('super_admin', $roles, true);
            if ($isSuper) {
                Response::json([
                    'municipios' => [],
                    'data' => [],
                    'isSuper' => true,
                    'scope' => 'global',
                ]);
                return;
            }
        }

        $rows = [];
        if (str_contains($identifier, '@')) {
            if ($user) {
                $rows = Database::fetchAll(
                    'SELECT DISTINCT m.*, um.principal
                       FROM usuario_municipios um
                       JOIN municipios m ON m.id = um.municipio_id
                      WHERE um.user_id = :user_id
                        AND um.ativo = true
                        AND m.ativo = true
                        AND m.bloqueado = false
                   ORDER BY um.principal DESC, m.nome',
                    ['user_id' => $user['id']]
                );
            }
        } else {
            $rows = Database::fetchAll(
                'SELECT DISTINCT m.*, um.principal
                   FROM usuario_municipios um
                   JOIN users u ON u.id = um.user_id
                   JOIN municipios m ON m.id = um.municipio_id
              LEFT JOIN profiles p ON p.user_id = u.id
                  WHERE COALESCE(um.ativo, true) = true
                    AND COALESCE(m.ativo, true) = true
                    AND COALESCE(m.bloqueado, false) = false
                    AND (
                        lower(um.username) = lower(:login)
                        OR lower(u.username) = lower(:login)
                        OR lower(split_part(u.email, \'@\', 1)) = lower(:login)
                        OR lower(p.username) = lower(:login)
                    )
                    AND COALESCE(p.ativo, u.is_active, true) = true
                    AND COALESCE(p.bloqueado, false) = false
               ORDER BY um.principal DESC, m.nome',
                ['login' => $identifier]
            );
        }

        // Fallback de compatibilidade: usuário antigo sem username em usuario_municipios.
        if (!$rows && $user) {
            $profile = Database::fetchOne('SELECT * FROM profiles WHERE user_id=:user_id LIMIT 1', ['user_id' => $user['id']]);
            if (!empty($profile['municipio_id'])) {
                $fallbackMunicipio = Database::fetchOne(
                    'SELECT *, true AS principal FROM municipios WHERE id=:id AND ativo=true AND bloqueado=false LIMIT 1',
                    ['id' => $profile['municipio_id']]
                );
                if ($fallbackMunicipio) {
                    $rows = [$fallbackMunicipio];
                }
            }
        }

        $municipios = array_map(fn($m) => [
            'id' => $m['id'],
            'nome' => $m['nome'],
            'name' => $m['nome'],
            'uf' => $m['uf'] ?? null,
            'slug' => $m['slug'] ?? null,
            'codigo' => $m['codigo'] ?? null,
            'codigo_ibge' => $m['codigo_ibge'] ?? null,
            'cnpj' => $m['cnpj'] ?? null,
            'principal' => filter_var($m['principal'] ?? false, FILTER_VALIDATE_BOOL),
        ], $rows);

        Response::json(['municipios' => $municipios, 'data' => $municipios, 'isSuper' => false, 'scope' => 'municipal']);
    }

    /**
     * Compatível com a Edge Function admin-create-user usada no frontend.
     * Cria users + profiles + usuario_municipios + user_roles e registra auditoria quando a tabela existir.
     */
    public function adminCreateUser(Request $request): void
    {
        $caller = Auth::requireUser($request);
        $body = $this->body($request);

        $email = strtolower(trim((string)($body['email'] ?? '')));
        $usuario = $this->normalizeUsername((string)($body['usuario'] ?? $body['username'] ?? $body['login'] ?? ''));
        if ($usuario === '' && $email !== '') {
            $usuario = $this->normalizeUsername(str_contains($email, '@') ? explode('@', $email)[0] : $email);
        }
        $nome = trim((string)($body['nome_completo'] ?? $body['name'] ?? $email));
        $senha = (string)($body['senha_temporaria'] ?? $body['password'] ?? '');
        $municipioId = $this->nullIfEmpty($body['municipio_id'] ?? null);
        $role = (string)($body['role'] ?? 'operador');
        $setorId = $this->nullIfEmpty($body['setor_id'] ?? null);
        $telefone = $this->nullIfEmpty($body['telefone'] ?? null);

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            Response::json(['error' => 'E-mail inválido'], 422);
            return;
        }
        if (!$this->validUsername($usuario)) {
            Response::json(['error' => 'Usuário inválido. Use letras, números, ponto, hífen ou underline. Ex: henrique.santos'], 422);
            return;
        }
        if (strlen($senha) < 6) {
            Response::json(['error' => 'A senha temporária precisa ter pelo menos 6 caracteres'], 422);
            return;
        }
        if (!$municipioId) {
            Response::json(['error' => 'municipio_id é obrigatório'], 422);
            return;
        }

        try {
            $this->assertRequiredTables(['users', 'profiles', 'municipios', 'usuario_municipios', 'user_roles']);

            $roles = array_column(Database::fetchAll('SELECT role FROM user_roles WHERE user_id=:id', ['id' => $caller['id']]), 'role');
            $isSuper = filter_var($caller['is_superuser'] ?? false, FILTER_VALIDATE_BOOL) || in_array('super_admin', $roles, true);
            if (!$isSuper) {
                $callerProfile = Database::fetchOne('SELECT municipio_id FROM profiles WHERE user_id=:id LIMIT 1', ['id' => $caller['id']]);
                $isAdminMunicipal = in_array('admin_municipal', $roles, true) && (($callerProfile['municipio_id'] ?? null) === $municipioId);
                if (!$isAdminMunicipal) {
                    Response::json(['error' => 'Sem permissão para criar usuário neste município'], 403);
                    return;
                }
                if ($role === 'super_admin') {
                    Response::json(['error' => 'Apenas Super Admin pode atribuir esse papel'], 403);
                    return;
                }
            }

            $muni = Database::fetchOne('SELECT * FROM municipios WHERE id=:id LIMIT 1', ['id' => $municipioId]);
            if (!$muni) {
                Response::json(['error' => 'Município inexistente'], 404);
                return;
            }
            if (filter_var($muni['bloqueado'] ?? false, FILTER_VALIDATE_BOOL)) {
                Response::json(['error' => 'Município bloqueado'], 403);
                return;
            }
            if (Database::fetchOne('SELECT id FROM users WHERE lower(email)=lower(:email) LIMIT 1', ['email' => $email])) {
                Response::json(['error' => 'E-mail já cadastrado'], 409);
                return;
            }
            if ($this->usernameExistsInMunicipio($usuario, $municipioId)) {
                Response::json(['error' => 'Já existe um usuário com este username neste município'], 409);
                return;
            }

            $limite = (int)($muni['limite_usuarios'] ?? 0);
            if ($limite > 0) {
                $used = Database::fetchOne('SELECT count(*)::int AS total FROM profiles WHERE municipio_id=:id', ['id' => $municipioId]);
                if ((int)($used['total'] ?? 0) >= $limite) {
                    Response::json(['error' => 'Limite de ' . $limite . ' usuários atingido'], 400);
                    return;
                }
            }

            $pdo = Database::pdo();
            $pdo->beginTransaction();

            $userId = $this->uuid();
            $profileId = $this->uuid();
            $now = date('Y-m-d H:i:s');
            $hash = $this->hashPassword($senha);

            $userData = [
                'id' => $userId,
                'name' => $nome,
                'email' => $email,
                'username' => $usuario,
                'usuario' => $usuario,
                'login' => $usuario,
                'nome_usuario' => $usuario,
                'password_hash' => $hash,
                'is_active' => true,
                'is_superuser' => $role === 'super_admin',
                'phone' => $telefone,
                'created_at' => $now,
                'updated_at' => $now,
            ];
            $this->insertDynamic('users', $userData, true);

            $profileData = [
                'id' => $profileId,
                'user_id' => $userId,
                'municipio_id' => $municipioId,
                'nome_completo' => $nome,
                'email' => $email,
                'username' => $usuario,
                'usuario' => $usuario,
                'login' => $usuario,
                'nome_usuario' => $usuario,
                'telefone' => $telefone,
                'setor_id' => $setorId,
                'ativo' => true,
                'bloqueado' => false,
                'senha_provisoria' => true,
                'created_at' => $now,
                'updated_at' => $now,
            ];
            $this->insertDynamic('profiles', $profileData, true);

            $this->insertDynamic('usuario_municipios', [
                'id' => $this->uuid(),
                'user_id' => $userId,
                'municipio_id' => $municipioId,
                'username' => $usuario,
                'email' => $email,
                'principal' => true,
                'ativo' => true,
                'created_at' => $now,
                'updated_at' => $now,
            ], true);

            $this->insertRole($userId, $role, $municipioId, $now);

            if ($this->hasTable('auditoria')) {
                $this->insertDynamic('auditoria', [
                    'id' => $this->uuid(),
                    'user_id' => $caller['id'],
                    'municipio_id' => $municipioId,
                    'acao' => 'create',
                    'entidade' => 'profiles',
                    'entidade_id' => $userId,
                    'descricao' => "Criou usuário {$usuario} / {$email} ({$role})",
                    'ip_address' => $request->clientIp(),
                    'user_agent' => $request->userAgent(),
                    'created_at' => $now,
                    'updated_at' => $now,
                ], false);
            }

            $pdo->commit();
            Response::json([
                'ok' => true,
                'data' => ['user_id' => $userId, 'profile_id' => $profileId, 'usuario' => $usuario],
                'user_id' => $userId,
                'profile_id' => $profileId,
                'usuario' => $usuario,
                'message' => 'Usuário criado com sucesso',
            ]);
        } catch (Throwable $e) {
            if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
                $pdo->rollBack();
            }
            $this->logError('admin-create-user', $e, $body);
            Response::json([
                'error' => 'Erro ao criar usuário',
                'message' => 'Erro ao criar usuário',
                'details' => $e->getMessage(),
                'file' => basename($e->getFile()),
                'line' => $e->getLine(),
            ], 500);
        }
    }

    public function adminUpdateUser(Request $request): void
    {
        Auth::requireUser($request);
        $body = $this->body($request);

        $targetUserId = (string)($body['target_user_id'] ?? $body['user_id'] ?? $body['id'] ?? '');
        $nome = $this->nullIfEmpty($body['nome_completo'] ?? $body['name'] ?? null);
        $email = strtolower(trim((string)($body['email'] ?? '')));
        $usuario = $this->normalizeUsername((string)($body['usuario'] ?? $body['username'] ?? $body['login'] ?? ''));
        $telefone = $this->nullIfEmpty($body['telefone'] ?? $body['phone'] ?? null);
        $setorId = $this->nullIfEmpty($body['setor_id'] ?? null);
        $role = $this->nullIfEmpty($body['role'] ?? null);

        // O município pode vir do formulário, do token ou do vínculo atual do usuário.
        $municipioId = $this->nullIfEmpty($body['municipio_id'] ?? $body['municipality_id'] ?? Auth::tokenMunicipality($request));

        if (!$targetUserId) {
            Response::json(['error' => 'target_user_id é obrigatório'], 422);
            return;
        }

        $pdo = Database::pdo();
        try {
            $pdo->beginTransaction();
            $now = date('Y-m-d H:i:s');

            // Se a tela não enviou municipio_id, descobre o município principal/atual do usuário.
            if (!$municipioId) {
                $municipioId = $this->resolveMunicipioForUserUpdate($targetUserId);
            }

            if ($usuario !== '' && !$this->validUsername($usuario)) {
                if ($pdo->inTransaction()) $pdo->rollBack();
                Response::json(['error' => 'Usuário inválido. Use letras, números, ponto, hífen ou underline.'], 422);
                return;
            }

            if ($usuario !== '') {
                if ($municipioId && $this->usernameExistsInMunicipio($usuario, $municipioId, $targetUserId)) {
                    if ($pdo->inTransaction()) $pdo->rollBack();
                    Response::json(['error' => 'Já existe um usuário com este username neste município'], 409);
                    return;
                }

                // Se o formulário não enviou municipio_id e o usuário tem vários vínculos,
                // valida duplicidade em todos os municípios vinculados antes de atualizar.
                if (!$municipioId) {
                    foreach ($this->linkedMunicipioIds($targetUserId) as $linkedMunicipioId) {
                        if ($this->usernameExistsInMunicipio($usuario, $linkedMunicipioId, $targetUserId)) {
                            if ($pdo->inTransaction()) $pdo->rollBack();
                            Response::json(['error' => 'Já existe um usuário com este username em um dos municípios vinculados'], 409);
                            return;
                        }
                    }
                }
            }

            // E-mail continua único globalmente em users.email. Permite manter o próprio e-mail.
            if ($email !== '') {
                $emailExists = Database::fetchOne(
                    'SELECT id FROM users WHERE lower(email)=lower(:email) AND id <> :id LIMIT 1',
                    ['email' => $email, 'id' => $targetUserId]
                );
                if ($emailExists) {
                    if ($pdo->inTransaction()) $pdo->rollBack();
                    Response::json(['error' => 'E-mail já cadastrado para outro usuário'], 409);
                    return;
                }
            }

            $userUpdates = [];
            if ($nome) $userUpdates['name'] = $nome;
            if ($email !== '') $userUpdates['email'] = $email;
            if ($usuario !== '') {
                // users.username fica como login preferencial/global, mas não é mais único globalmente.
                $userUpdates['username'] = $usuario;
                $userUpdates['usuario'] = $usuario;
                $userUpdates['login'] = $usuario;
                $userUpdates['nome_usuario'] = $usuario;
            }
            if ($telefone !== null) $userUpdates['phone'] = $telefone;
            if ($userUpdates) {
                $userUpdates['updated_at'] = $now;
                $this->updateDynamic('users', $userUpdates, ['id' => $targetUserId]);
            }

            $profileUpdates = [];
            if ($nome) $profileUpdates['nome_completo'] = $nome;
            if ($email !== '') $profileUpdates['email'] = $email;
            if ($usuario !== '') {
                $profileUpdates['username'] = $usuario;
                $profileUpdates['usuario'] = $usuario;
                $profileUpdates['login'] = $usuario;
                $profileUpdates['nome_usuario'] = $usuario;
            }
            $profileUpdates['telefone'] = $telefone;
            $profileUpdates['setor_id'] = $setorId;
            $profileUpdates['updated_at'] = $now;

            // profiles é 1 por user_id no seu DDL. Atualiza por user_id para não perder quando municipio_id vier vazio/diferente.
            $this->updateDynamic('profiles', $profileUpdates, ['user_id' => $targetUserId]);

            // Correção principal: sempre sincroniza usuario_municipios.
            // Essa tabela é a fonte da regra username + município.
            // Quando municipio_id vem na requisição, atualiza/cria o vínculo daquele município.
            // Quando não vem, atualiza todos os vínculos municipais existentes do usuário.
            if ($municipioId) {
                $this->syncUsuarioMunicipioCredentials($targetUserId, $municipioId, $usuario, $email, $now);
            } else {
                $this->syncAllUsuarioMunicipioCredentials($targetUserId, $usuario, $email, $now);
            }

            if ($role) {
                if ($municipioId && $this->hasColumn('user_roles', 'municipio_id')) {
                    Database::execute('DELETE FROM user_roles WHERE user_id=:u AND municipio_id=:m', ['u' => $targetUserId, 'm' => $municipioId]);
                } else {
                    Database::execute('DELETE FROM user_roles WHERE user_id=:u', ['u' => $targetUserId]);
                }
                $this->insertRole($targetUserId, $role, $municipioId, $now);
            }

            $pdo->commit();
            Response::json([
                'ok' => true,
                'data' => [
                    'user_id' => $targetUserId,
                    'municipio_id' => $municipioId,
                    'usuario' => $usuario !== '' ? $usuario : null,
                    'username' => $usuario !== '' ? $usuario : null,
                ],
                'message' => 'Usuário atualizado com sucesso'
            ]);
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $this->logError('admin-update-user', $e, $body);
            Response::json([
                'error' => 'Erro ao atualizar usuário',
                'message' => $e->getMessage(),
            ], 500);
        }
    }

    public function adminResetPassword(Request $request): void
    {
        Auth::requireUser($request);
        $body = $this->body($request);

        $targetUserId = (string)($body['target_user_id'] ?? '');
        $novaSenha = (string)($body['nova_senha'] ?? '');
        if (!$targetUserId || strlen($novaSenha) < 6) {
            Response::json(['error' => 'Dados inválidos para reset de senha'], 422);
            return;
        }

        $hash = $this->hashPassword($novaSenha);
        $now = date('Y-m-d H:i:s');
        Database::execute('UPDATE users SET password_hash=:h, updated_at=:now WHERE id=:id', ['h' => $hash, 'now' => $now, 'id' => $targetUserId]);

        if ($this->hasColumn('profiles', 'senha_provisoria')) {
            Database::execute('UPDATE profiles SET senha_provisoria=true, updated_at=:now WHERE user_id=:id', ['now' => $now, 'id' => $targetUserId]);
        }

        Response::json(['data' => ['user_id' => $targetUserId], 'message' => 'Senha resetada com sucesso']);
    }

    private function body(Request $request): array
    {
        return is_array($request->body) ? $request->body : [];
    }

    private function resolveUserAndProfileByIdentifier(string $identifier): array
    {
        $identifier = strtolower(trim($identifier));
        if ($identifier === '') return ['user' => null, 'profile' => null];

        $conditions = [];
        $params = ['login' => $identifier];

        if ($this->hasColumn('users', 'email')) {
            $conditions[] = 'lower(u.email) = lower(:login)';
            $conditions[] = "lower(split_part(u.email, '@', 1)) = lower(:login)";
        }

        foreach ($this->loginColumns('users') as $column) {
            $conditions[] = 'lower(u.' . $this->qi($column) . ') = lower(:login)';
        }

        if ($this->hasColumn('profiles', 'user_id')) {
            if ($this->hasColumn('profiles', 'email')) {
                $conditions[] = 'lower(p.email) = lower(:login)';
            }
            foreach ($this->loginColumns('profiles') as $column) {
                $conditions[] = 'lower(p.' . $this->qi($column) . ') = lower(:login)';
            }
        }

        $conditions = array_values(array_unique($conditions));
        if (!$conditions) return ['user' => null, 'profile' => null];

        $join = $this->hasColumn('profiles', 'user_id') ? ' LEFT JOIN profiles p ON p.user_id = u.id ' : ' ';
        $user = Database::fetchOne(
            'SELECT u.* FROM users u' . $join .
            'WHERE ' . implode(' OR ', $conditions) . ' LIMIT 1',
            $params
        );

        if (!$user) {
            return ['user' => null, 'profile' => null];
        }

        $profile = $this->hasColumn('profiles', 'user_id')
            ? Database::fetchOne('SELECT * FROM profiles WHERE user_id=:user_id LIMIT 1', ['user_id' => $user['id']])
            : null;

        return ['user' => $user, 'profile' => $profile];
    }

    private function findUserByIdentifier(string $identifier): ?array
    {
        return $this->resolveUserAndProfileByIdentifier($identifier)['user'] ?? null;
    }

    private function resolveMunicipioForUserUpdate(string $userId): ?string
    {
        // 1) vínculo principal em usuario_municipios
        if ($this->hasTable('usuario_municipios')) {
            $row = Database::fetchOne(
                'SELECT municipio_id
                   FROM usuario_municipios
                  WHERE user_id = :user_id
                  ORDER BY principal DESC, updated_at DESC
                  LIMIT 1',
                ['user_id' => $userId]
            );
            if (!empty($row['municipio_id'])) return (string)$row['municipio_id'];
        }

        // 2) profile do usuário
        if ($this->hasTable('profiles') && $this->hasColumn('profiles', 'municipio_id')) {
            $row = Database::fetchOne(
                'SELECT municipio_id FROM profiles WHERE user_id = :user_id LIMIT 1',
                ['user_id' => $userId]
            );
            if (!empty($row['municipio_id'])) return (string)$row['municipio_id'];
        }

        // 3) role municipal
        if ($this->hasTable('user_roles') && $this->hasColumn('user_roles', 'municipio_id')) {
            $row = Database::fetchOne(
                'SELECT municipio_id
                   FROM user_roles
                  WHERE user_id = :user_id AND municipio_id IS NOT NULL
                  ORDER BY updated_at DESC
                  LIMIT 1',
                ['user_id' => $userId]
            );
            if (!empty($row['municipio_id'])) return (string)$row['municipio_id'];
        }

        return null;
    }

    private function syncUsuarioMunicipioCredentials(string $userId, string $municipioId, string $username, string $email, string $now): void
    {
        if (!$this->hasTable('usuario_municipios')) return;

        $existing = Database::fetchOne(
            'SELECT id FROM usuario_municipios WHERE user_id = :user_id AND municipio_id = :municipio_id LIMIT 1',
            ['user_id' => $userId, 'municipio_id' => $municipioId]
        );

        if ($existing) {
            $updates = ['updated_at' => $now];
            if ($username !== '') $updates['username'] = $username;
            if ($email !== '') $updates['email'] = $email;
            $this->updateDynamic('usuario_municipios', $updates, ['user_id' => $userId, 'municipio_id' => $municipioId]);
            return;
        }

        // Se por algum motivo o vínculo não existir, cria o vínculo para não deixar users/profiles sem usuario_municipios.
        $data = [
            'id' => $this->uuid(),
            'user_id' => $userId,
            'municipio_id' => $municipioId,
            'username' => $username !== '' ? $username : null,
            'email' => $email !== '' ? $email : null,
            'principal' => false,
            'ativo' => true,
            'created_at' => $now,
            'updated_at' => $now,
        ];
        $this->insertDynamic('usuario_municipios', $data, true);
    }

    private function linkedMunicipioIds(string $userId): array
    {
        if (!$this->hasTable('usuario_municipios')) return [];

        $rows = Database::fetchAll(
            'SELECT municipio_id
               FROM usuario_municipios
              WHERE user_id = :user_id
                AND municipio_id IS NOT NULL',
            ['user_id' => $userId]
        );

        return array_values(array_filter(array_map(fn($row) => (string)($row['municipio_id'] ?? ''), $rows)));
    }

    private function syncAllUsuarioMunicipioCredentials(string $userId, string $username, string $email, string $now): void
    {
        if (!$this->hasTable('usuario_municipios')) return;

        $updates = ['updated_at' => $now];
        if ($username !== '') $updates['username'] = $username;
        if ($email !== '') $updates['email'] = $email;

        // Se não existe nada para atualizar além do timestamp, ainda assim atualiza updated_at
        // para deixar claro que o vínculo foi sincronizado.
        foreach ($this->linkedMunicipioIds($userId) as $municipioId) {
            $this->updateDynamic('usuario_municipios', $updates, [
                'user_id' => $userId,
                'municipio_id' => $municipioId,
            ]);
        }
    }

    private function usernameExistsInMunicipio(string $username, ?string $municipioId, ?string $exceptUserId = null): bool
    {
        $username = strtolower(trim($username));
        if ($username === '' || !$municipioId) return false;

        $params = ['login' => $username, 'municipio_id' => $municipioId];
        $sql = 'SELECT user_id
                  FROM usuario_municipios
                 WHERE municipio_id = :municipio_id
                   AND lower(username) = lower(:login)';
        if ($exceptUserId) {
            $sql .= ' AND user_id <> :except';
            $params['except'] = $exceptUserId;
        }
        $sql .= ' LIMIT 1';

        return (bool)Database::fetchOne($sql, $params);
    }

    private function findGlobalUserByUsername(string $username): ?array
    {
        $username = strtolower(trim($username));
        if ($username === '') return null;

        foreach ($this->loginColumns('users') as $column) {
            $user = Database::fetchOne(
                'SELECT u.*
                   FROM users u
                  WHERE lower(u.' . $this->qi($column) . ') = lower(:login)
                    AND (u.is_superuser = true
                         OR EXISTS (SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = \'super_admin\'))
                  LIMIT 1',
                ['login' => $username]
            );
            if ($user) return $user;
        }

        return null;
    }

    private function normalizeUsername(string $username): string
    {
        $username = strtolower(trim($username));
        $username = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $username) ?: $username;
        $username = preg_replace('/\s+/', '.', $username) ?? $username;
        $username = preg_replace('/[^a-z0-9._-]/', '', $username) ?? $username;
        $username = preg_replace('/\.+/', '.', $username) ?? $username;
        return trim($username, '.-_');
    }

    private function validUsername(string $username): bool
    {
        return (bool)preg_match('/^[a-z0-9][a-z0-9._-]{2,59}$/', $username);
    }

    private function loginColumns(string $table): array
    {
        return array_values(array_filter(['username', 'usuario', 'login', 'nome_usuario'], fn($column) => $this->hasColumn($table, $column)));
    }

    private function hashPassword(string $password): string
    {
        if (defined('PASSWORD_ARGON2ID')) {
            $hash = @password_hash($password, PASSWORD_ARGON2ID);
            if (is_string($hash) && $hash !== '') return $hash;
        }
        return password_hash($password, PASSWORD_BCRYPT);
    }

    private function insertRole(string $userId, string $role, ?string $municipioId, string $now): void
    {
        $data = ['user_id' => $userId, 'role' => $role, 'created_at' => $now, 'updated_at' => $now];
        if ($this->hasColumn('user_roles', 'id')) $data['id'] = $this->uuid();
        if ($this->hasColumn('user_roles', 'municipio_id')) $data['municipio_id'] = $municipioId;
        $this->insertDynamic('user_roles', $data, true);
    }

    private function insertDynamic(string $table, array $data, bool $throwIfEmpty = false): void
    {
        $data = $this->clean($table, $data);
        if (!$data) {
            if ($throwIfEmpty) throw new \RuntimeException("Nenhuma coluna compatível encontrada para inserir em {$table}");
            return;
        }
        $cols = array_keys($data);
        $sql = 'INSERT INTO ' . $this->qi($table) . ' (' . implode(',', array_map([$this, 'qi'], $cols)) . ') VALUES (' . implode(',', array_map(fn($c) => ':' . $c, $cols)) . ')';
        $stmt = Database::pdo()->prepare($sql);
        $stmt->execute($this->normalizeParams($data));
    }

    private function updateDynamic(string $table, array $data, array $where): void
    {
        $data = $this->clean($table, $data);
        $where = $this->clean($table, $where);
        if (!$data || !$where) return;
        $sets = [];
        $bind = [];
        foreach ($data as $col => $val) {
            $sets[] = $this->qi($col) . ' = :set_' . $col;
            $bind['set_' . $col] = $val;
        }
        $w = [];
        foreach ($where as $col => $val) {
            $w[] = $this->qi($col) . ' = :where_' . $col;
            $bind['where_' . $col] = $val;
        }
        $sql = 'UPDATE ' . $this->qi($table) . ' SET ' . implode(', ', $sets) . ' WHERE ' . implode(' AND ', $w);
        $stmt = Database::pdo()->prepare($sql);
        $stmt->execute($this->normalizeParams($bind));
    }

    private function normalizeParams(array $params): array
    {
        foreach ($params as $key => $value) {
            if (is_bool($value)) {
                $params[$key] = $value ? 'true' : 'false';
            }
        }
        return $params;
    }

    private function assertRequiredTables(array $tables): void
    {
        foreach ($tables as $table) {
            if (!$this->hasTable($table)) {
                throw new \RuntimeException("Tabela obrigatória não encontrada no schema atual: {$table}");
            }
        }
    }

    private function hasTable(string $table): bool
    {
        return Database::tableExists($table);
    }

    private function clean(string $table, array $data): array
    {
        return array_intersect_key($data, array_flip($this->columns($table)));
    }

    private function columns(string $table): array
    {
        return Database::tableColumns($table);
    }

    private function hasColumn(string $table, string $column): bool
    {
        return in_array($column, $this->columns($table), true);
    }

    private function nullIfEmpty(mixed $value): ?string
    {
        if ($value === null) return null;
        $value = trim((string)$value);
        return $value === '' ? null : $value;
    }

    private function qi(string $id): string
    {
        return '"' . str_replace('"', '""', $id) . '"';
    }

    private function uuid(): string
    {
        $data = random_bytes(16);
        $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
        $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }

    private function logError(string $context, Throwable $e, array $body = []): void
    {
        $root = dirname(__DIR__, 2);
        $dir = $root . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'logs';
        if (!is_dir($dir)) @mkdir($dir, 0777, true);
        $safeBody = $body;
        unset($safeBody['senha_temporaria'], $safeBody['password'], $safeBody['nova_senha']);
        $line = '[' . date('c') . "] {$context}: " . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . ' body=' . json_encode($safeBody, JSON_UNESCAPED_UNICODE) . PHP_EOL;
        @file_put_contents($dir . DIRECTORY_SEPARATOR . 'api-error.log', $line, FILE_APPEND);
    }
}
