<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Auth;
use App\Core\Database;
use App\Core\Env;
use App\Core\Jwt;
use App\Core\Request;
use App\Core\Response;
use App\Core\UrlHelper;

final class AuthController
{
    public function login(Request $request): void
    {
        try {
            $identifier = $this->loginIdentifier($request);
            $password = (string)($request->body['password'] ?? '');
            $municipalityId = $request->body['municipality_id'] ?? $request->body['municipio_id'] ?? null;

            if ($identifier === '' || $password === '') {
                Response::error('Informe usuário e senha', 422);
                return;
            }

            $user = $this->findUserForLogin($identifier, $municipalityId ? (string)$municipalityId : null);
            $ok = false;
            if ($user && ($user['is_active'] ?? true)) {
                $hash = (string)($user['password_hash'] ?? '');
                $ok = $hash !== '' && password_verify($password, $hash);
            }

            $this->recordAttempt($identifier, $request, $ok);

            if (!$ok || !$user) {
                Response::error('Credenciais inválidas', 401);
                return;
            }

            $isSuper = $this->isSuperUser($user);

            // Para usuário global/super_admin, não carregue todos os municípios no login.
            // Isso evita travar o pós-login e mantém o comportamento parecido com o FastAPI.
            $available = $isSuper ? [] : $this->availableMunicipalities($user);
            $active = $isSuper
                ? ($municipalityId ? $this->findActiveMunicipality((string)$municipalityId, 'super_admin') : null)
                : $this->resolveActiveMunicipality($available, $municipalityId);
            $activeId = $active['id'] ?? null;

            $access = Jwt::encode([
                'sub' => $user['id'],
                'municipality_id' => $activeId,
                'email' => $user['email'] ?? null,
                'username' => $this->usernameFromUser($user),
            ], Env::int('ACCESS_TOKEN_EXPIRE_MINUTES', 480) * 60);
            $refresh = Jwt::encode([
                'sub' => $user['id'],
                'municipality_id' => $activeId,
                'type' => 'refresh',
            ], Env::int('REFRESH_TOKEN_EXPIRE_DAYS', 7) * 86400);

            $this->recordSession((string)$user['id'], $activeId ? (string)$activeId : null, $refresh, $request);

            $profile = $this->safeProfile((string)$user['id']);
            $roles = $this->userRoles((string)$user['id']);

            Response::json([
                'access_token' => $access,
                'refresh_token' => $refresh,
                'token_type' => 'bearer',
                'user' => $this->frontendUser($user),
                'profile' => $this->profilePayload($user, $profile),
                'roles' => array_values(array_unique($roles)),
                'is_super' => $isSuper,
                'isSuper' => $isSuper,
                'scope' => $isSuper ? 'global' : 'municipal',
                'available_municipalities' => $available,
                'active_municipality' => $active,
                'branding' => $this->branding($activeId),
                'groups' => $this->groups($activeId),
                'permissions' => $this->permissions((string)$user['id'], $activeId),
                'modules' => $this->modules($activeId),
            ]);
        } catch (\Throwable $e) {
            $this->logError('auth-login', $e, $request->body ?? []);
            Response::error('Falha ao realizar login', 500, Env::bool('APP_DEBUG') ? $e->getMessage() : null);
        }
    }

    public function me(Request $request): void
    {
        $user = Auth::requireUser($request);
        $profile = $this->safeProfile((string)$user['id']);
        $roles = $this->userRoles((string)$user['id']);
        Response::json([
            'user' => $this->frontendUser($user),
            'profile' => $this->profilePayload($user, $profile),
            'roles' => array_values(array_unique($roles)),
        ]);
    }

    public function session(Request $request): void
    {
        $user = Auth::requireUser($request);
        Response::json(['session' => ['user' => $this->frontendUser($user)]]);
    }

    public function logout(Request $request): void
    {
        $refresh = $request->body['refresh_token'] ?? $request->header('x-refresh-token');
        if ($refresh) {
            Database::execute('UPDATE sessoes_login SET encerrada_em = :now WHERE refresh_token_hash = :hash AND encerrada_em IS NULL', [
                'now' => date(DATE_ATOM),
                'hash' => hash('sha256', (string)$refresh),
            ]);
        }
        Response::success(null, 'Logout realizado com sucesso');
    }

    public function myMunicipalities(Request $request): void
    {
        $user = Auth::requireUser($request);
        Response::success($this->availableMunicipalities($user), 'Consulta realizada com sucesso');
    }

    public function selectMunicipality(Request $request): void
    {
        $user = Auth::requireUser($request);
        $municipalityId = $request->body['municipality_id'] ?? $request->body['municipio_id'] ?? null;
        if ($this->isSuperUser($user)) {
            $active = $municipalityId ? $this->findActiveMunicipality((string)$municipalityId, 'super_admin') : null;
            if ($municipalityId && !$active) {
                Response::error('MunicÃ­pio nÃ£o encontrado ou indisponÃ­vel', 404);
                return;
            }
        } else {
            $available = $this->availableMunicipalities($user);
            $active = $this->resolveActiveMunicipality($available, $municipalityId);
        }
        $activeId = $active['id'] ?? null;
        $role = $this->primaryRole((string)$user['id'], $activeId ? (string)$activeId : null);
        $roles = $this->userRoles((string)$user['id']);
        if ($role !== '' && !in_array($role, $roles, true)) {
            $roles[] = $role;
        }

        $access = Jwt::encode([
            'sub' => $user['id'],
            'municipality_id' => $activeId,
            'municipio_id' => $activeId,
            'email' => $user['email'] ?? null,
            'username' => $this->usernameFromUser($user),
            'role' => $role,
            'roles' => array_values(array_unique($roles)),
        ], Env::int('ACCESS_TOKEN_EXPIRE_MINUTES', 480) * 60);
        $refresh = Jwt::encode([
            'sub' => $user['id'],
            'municipality_id' => $activeId,
            'municipio_id' => $activeId,
            'type' => 'refresh',
        ], Env::int('REFRESH_TOKEN_EXPIRE_DAYS', 7) * 86400);

        $this->recordSession((string)$user['id'], $activeId ? (string)$activeId : null, $refresh, $request);

        Response::success([
            'access_token' => $access,
            'refresh_token' => $refresh,
            'token_type' => 'bearer',
            'active_municipality' => $active,
            'roles' => array_values(array_unique($roles)),
            'role' => $role,
            'branding' => $this->branding($activeId),
            'groups' => $this->groups($activeId),
            'permissions' => $this->permissions($user['id'], $activeId),
            'modules' => $this->modules($activeId),
        ], 'Município selecionado com sucesso');
    }

    public function signup(Request $request): void
    {
        $email = strtolower(trim((string)($request->body['email'] ?? '')));
        $password = (string)($request->body['password'] ?? '');
        $name = $request->body['nome_completo'] ?? $email;
        $municipioId = $request->body['municipio_id'] ?? null;
        if (!$email || strlen($password) < 8) {
            Response::error('Dados inválidos', 400);
            return;
        }
        if (Database::fetchOne('SELECT id FROM users WHERE lower(email)=lower(:email)', ['email' => $email])) {
            Response::error('E-mail já cadastrado', 400);
            return;
        }
        $id = $this->uuid();
        $hash = password_hash($password, PASSWORD_ARGON2ID);
        Database::execute('INSERT INTO users (id, name, email, password_hash, is_active, is_superuser, created_at, updated_at) VALUES (:id,:name,:email,:hash,true,false,now(),now())', [
            'id' => $id, 'name' => $name, 'email' => $email, 'hash' => $hash,
        ]);
        Database::execute('INSERT INTO profiles (id, user_id, municipio_id, nome_completo, email, ativo, bloqueado, senha_provisoria, created_at, updated_at) VALUES (:id,:user_id,:municipio_id,:nome,:email,true,false,false,now(),now())', [
            'id' => $this->uuid(), 'user_id' => $id, 'municipio_id' => $municipioId, 'nome' => $name, 'email' => $email,
        ]);
        Response::success(['user_id' => $id], 'Cadastro realizado com sucesso');
    }

    public function resetPasswordEmail(Request $request): void
    {
        Response::success(['sent' => true, 'email' => $request->body['email'] ?? null], 'Solicitação registrada com sucesso');
    }

    public function updateUser(Request $request): void
    {
        $user = Auth::requireUser($request);
        $fields = [];
        $params = ['id' => $user['id']];
        if (!empty($request->body['email'])) {
            $fields[] = 'email = :email';
            $params['email'] = strtolower(trim((string)$request->body['email']));
        }
        if (!empty($request->body['password'])) {
            $fields[] = 'password_hash = :password_hash';
            $params['password_hash'] = password_hash((string)$request->body['password'], PASSWORD_ARGON2ID);
        }
        if ($fields) {
            Database::execute('UPDATE users SET ' . implode(', ', $fields) . ', updated_at = now() WHERE id = :id', $params);
        }
        Response::success(['user_id' => $user['id']], 'Usuário atualizado com sucesso');
    }

    private function frontendUser(array $user): array
    {
        $username = $this->usernameFromUser($user);
        return [
            'id' => $user['id'],
            'email' => $user['email'],
            'username' => $username,
            'usuario' => $username,
            'user_metadata' => [
                'name' => $user['name'] ?? $user['email'],
                'username' => $username,
                'usuario' => $username,
            ],
        ];
    }

    private function profilePayload(array $user, ?array $profile): ?array
    {
        if (!$profile) return null;
        return [
            'id' => $profile['id'] ?? $user['id'],
            'user_id' => $user['id'],
            'municipio_id' => $profile['municipio_id'] ?? null,
            'nome_completo' => $profile['nome_completo'] ?? $user['name'],
            'email' => $user['email'],
            'username' => $this->usernameFromUserProfile($user, $profile),
            'usuario' => $this->usernameFromUserProfile($user, $profile),
            'ativo' => $profile['ativo'] ?? $user['is_active'],
            'bloqueado' => $profile['bloqueado'] ?? false,
            'senha_provisoria' => $profile['senha_provisoria'] ?? false,
        ];
    }

    private function loginIdentifier(Request $request): string
    {
        return strtolower(trim((string)(
            $request->body['usuario']
            ?? $request->body['username']
            ?? $request->body['login']
            ?? $request->body['email']
            ?? ''
        )));
    }

    private function findUserForLogin(string $identifier, ?string $municipioId): ?array
    {
        $identifier = strtolower(trim($identifier));
        if ($identifier === '') {
            return null;
        }

        // Quando houver município selecionado, resolve o usuário pelo vínculo municipal.
        // Isso permite o mesmo username em municípios diferentes.
        if ($municipioId && !str_contains($identifier, '@') && $this->tableExists('usuario_municipios')) {
            try {
                $conditions = [];
                if ($this->hasColumn('usuario_municipios', 'username')) {
                    $conditions[] = 'lower(um.username) = lower(:login)';
                }
                if ($this->hasColumn('users', 'username')) {
                    $conditions[] = 'lower(u.username) = lower(:login)';
                }
                if ($this->hasColumn('users', 'email')) {
                    $conditions[] = "lower(split_part(u.email, '@', 1)) = lower(:login)";
                }
                if ($this->hasColumn('profiles', 'username')) {
                    $conditions[] = 'lower(p.username) = lower(:login)';
                }

                if ($conditions) {
                    $where = [
                        'um.municipio_id = :municipio_id',
                        '(' . implode(' OR ', array_values(array_unique($conditions))) . ')',
                    ];
                    if ($this->hasColumn('usuario_municipios', 'ativo')) {
                        $where[] = 'COALESCE(um.ativo, true) = true';
                    }
                    if ($this->hasColumn('users', 'is_active')) {
                        $where[] = 'COALESCE(u.is_active, true) = true';
                    }
                    if ($this->hasColumn('profiles', 'bloqueado')) {
                        $where[] = 'COALESCE(p.bloqueado, false) = false';
                    }

                    $user = Database::fetchOne(
                        'SELECT u.*
                           FROM usuario_municipios um
                           JOIN users u ON u.id = um.user_id
                      LEFT JOIN profiles p ON p.user_id = u.id
                          WHERE ' . implode(' AND ', $where) . '
                          LIMIT 1',
                        ['login' => $identifier, 'municipio_id' => $municipioId]
                    );
                    if ($user) {
                        return $user;
                    }
                }
            } catch (\Throwable $e) {
                $this->logError('auth-find-user-municipal', $e, ['identifier' => $identifier, 'municipio_id' => $municipioId]);
            }
        }

        return $this->findUserByIdentifier($identifier);
    }

    private function findUserByIdentifier(string $identifier): ?array
    {
        $identifier = strtolower(trim($identifier));
        if ($identifier === '') {
            return 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 null;

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

    private function usernameFromUser(array $user): ?string
    {
        foreach (['username', 'usuario', 'login', 'nome_usuario'] as $column) {
            if (!empty($user[$column])) return (string)$user[$column];
        }
        if (!empty($user['email']) && str_contains((string)$user['email'], '@')) {
            return strtolower(explode('@', (string)$user['email'])[0]);
        }
        return null;
    }

    private function usernameFromUserProfile(array $user, ?array $profile): ?string
    {
        foreach (['username', 'usuario', 'login', 'nome_usuario'] as $column) {
            if (!empty($profile[$column])) return (string)$profile[$column];
            if (!empty($user[$column])) return (string)$user[$column];
        }
        return $this->usernameFromUser($user);
    }

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

    private function safeProfile(string $userId): ?array
    {
        if (!$this->tableExists('profiles') || !$this->hasColumn('profiles', 'user_id')) {
            return null;
        }
        try {
            return Database::fetchOne('SELECT * FROM profiles WHERE user_id = :id LIMIT 1', ['id' => $userId]);
        } catch (\Throwable) {
            return null;
        }
    }

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

    private function hasColumn(string $table, string $column): bool
    {
        return in_array($column, Database::tableColumns($table), true);
    }

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

    private function userRoles(string $userId): array
    {
        if (!$this->tableExists('user_roles') || !$this->hasColumn('user_roles', 'user_id') || !$this->hasColumn('user_roles', 'role')) {
            return [];
        }

        try {
            return array_column(
                Database::fetchAll('SELECT role FROM user_roles WHERE user_id = :id ORDER BY role', ['id' => $userId]),
                'role'
            );
        } catch (\Throwable) {
            return [];
        }
    }

    private function isSuperUser(array $user): bool
    {
        $roles = $this->userRoles((string)$user['id']);
        return filter_var($user['is_superuser'] ?? false, FILTER_VALIDATE_BOOL) || in_array('super_admin', $roles, true);
    }

    private function findActiveMunicipality(string $id, string $role = 'super_admin'): ?array
    {
        if (!$this->tableExists('municipios') || !$this->hasColumn('municipios', 'id')) {
            return null;
        }

        $where = ['id = :id'];
        if ($this->hasColumn('municipios', 'ativo')) {
            $where[] = 'ativo = true';
        }
        if ($this->hasColumn('municipios', 'bloqueado')) {
            $where[] = 'COALESCE(bloqueado, false) = false';
        }

        try {
            $m = Database::fetchOne('SELECT * FROM municipios WHERE ' . implode(' AND ', $where) . ' LIMIT 1', ['id' => $id]);
            return $m ? $this->municipalityPayload($m, $role) : null;
        } catch (\Throwable) {
            return null;
        }
    }

    private function availableMunicipalities(array $user): array
    {
        if (!$this->tableExists('municipios')) {
            return [];
        }

        if ($this->isSuperUser($user)) {
            // Usuário global não deve carregar todos os municípios no login.
            // A lista completa deve ser carregada apenas quando abrir a seleção de município.
            $where = [];
            if ($this->hasColumn('municipios', 'ativo')) {
                $where[] = 'COALESCE(ativo, true) = true';
            }
            if ($this->hasColumn('municipios', 'bloqueado')) {
                $where[] = 'COALESCE(bloqueado, false) = false';
            }

            $sql = 'SELECT * FROM municipios';
            if ($where) {
                $sql .= ' WHERE ' . implode(' AND ', $where);
            }
            if ($this->hasColumn('municipios', 'nome')) {
                $sql .= ' ORDER BY nome';
            }

            try {
                $rows = Database::fetchAll($sql);
                return array_map(fn($m) => $this->municipalityPayload($m, 'super_admin'), $rows);
            } catch (\Throwable) {
                return [];
            }
        }

        if (!$this->tableExists('usuario_municipios') || !$this->hasColumn('usuario_municipios', 'user_id') || !$this->hasColumn('usuario_municipios', 'municipio_id')) {
            return [];
        }

        $where = ['um.user_id = :id'];
        if ($this->hasColumn('usuario_municipios', 'ativo')) {
            $where[] = 'COALESCE(um.ativo, true) = true';
        }
        if ($this->hasColumn('municipios', 'ativo')) {
            $where[] = 'COALESCE(m.ativo, true) = true';
        }
        if ($this->hasColumn('municipios', 'bloqueado')) {
            $where[] = 'COALESCE(m.bloqueado, false) = false';
        }

        $order = [];
        if ($this->hasColumn('usuario_municipios', 'principal')) {
            $order[] = 'um.principal DESC';
        }
        if ($this->hasColumn('municipios', 'nome')) {
            $order[] = 'm.nome ASC';
        }

        try {
            // Compatível com PostgreSQL antigo: primeiro carrega municípios, depois resolve roles em PHP.
            $rows = Database::fetchAll(
                'SELECT m.*, ' . ($this->hasColumn('usuario_municipios', 'principal') ? 'um.principal' : 'false AS principal') . '
                   FROM usuario_municipios um
                   JOIN municipios m ON m.id = um.municipio_id
                  WHERE ' . implode(' AND ', $where) . ($order ? ' ORDER BY ' . implode(', ', $order) : ''),
                ['id' => $user['id']]
            );
        } catch (\Throwable $e) {
            $this->logError('auth-available-municipalities', $e, ['user_id' => $user['id'] ?? null]);
            return [];
        }

        $rolesByMunicipio = $this->rolesByMunicipio((string)$user['id']);

        return array_map(function ($m) use ($rolesByMunicipio) {
            $mid = (string)($m['id'] ?? '');
            $role = $rolesByMunicipio[$mid] ?? $rolesByMunicipio['__global__'] ?? 'leitor';
            return $this->municipalityPayload($m, $role);
        }, $rows);
    }

    private function rolesByMunicipio(string $userId): array
    {
        if (!$this->tableExists('user_roles') || !$this->hasColumn('user_roles', 'user_id') || !$this->hasColumn('user_roles', 'role')) {
            return [];
        }

        try {
            $rows = Database::fetchAll(
                'SELECT role, municipio_id
                   FROM user_roles
                  WHERE user_id = :id',
                ['id' => $userId]
            );
        } catch (\Throwable) {
            return [];
        }

        $priority = [
            'super_admin' => 1,
            'admin_municipal' => 2,
            'gestor' => 3,
            'operador' => 4,
            'leitor' => 5,
        ];

        $out = [];
        $outPriority = [];
        foreach ($rows as $row) {
            $role = (string)($row['role'] ?? '');
            if ($role === '') continue;
            $key = !empty($row['municipio_id']) ? (string)$row['municipio_id'] : '__global__';
            $p = $priority[$role] ?? 99;
            if (!isset($outPriority[$key]) || $p < $outPriority[$key]) {
                $out[$key] = $role;
                $outPriority[$key] = $p;
            }
        }
        return $out;
    }

    private function resolveActiveMunicipality(array $available, ?string $id): ?array
    {
        if (!$available) return null;
        if ($id) {
            foreach ($available as $m) if ((string)$m['id'] === (string)$id) return $m;
            Response::error('Usuário não possui acesso a este município', 401); exit;
        }
        return $available[0];
    }

    private function municipalityPayload(array $m, string $role): array
    {
        return [
            'id' => $m['id'], 'name' => $m['nome'], 'nome' => $m['nome'], 'uf' => $m['uf'] ?? null,
            'slug' => $m['slug'] ?? null, 'code' => $m['codigo'] ?? null, 'codigo' => $m['codigo'] ?? null,
            'codigo_ibge' => $m['codigo_ibge'] ?? null, 'cnpj' => $m['cnpj'] ?? null,
            'role_name' => $role, 'role' => $role, 'schema_name' => null,
            'ativo' => $m['ativo'] ?? true, 'bloqueado' => $m['bloqueado'] ?? false,
        ];
    }

    private function primaryRole(string $userId, ?string $municipioId): string
    {
        $rows = Database::fetchAll('SELECT role FROM user_roles WHERE user_id = :u AND (municipio_id = :m OR municipio_id IS NULL)', ['u' => $userId, 'm' => $municipioId]);
        $roles = array_column($rows, 'role');
        foreach (['super_admin','admin_municipal','gestor','operador','leitor'] as $role) {
            if (in_array($role, $roles, true)) return $role;
        }
        return $roles[0] ?? 'leitor';
    }

    private function branding(?string $municipioId): ?array
    {
        if (!$municipioId || !$this->tableExists('branding_municipio') || !$this->hasColumn('branding_municipio', 'municipio_id')) {
            return null;
        }
        try {
            $where = 'municipio_id=:id';
            if ($this->hasColumn('branding_municipio', 'ativo')) {
                $where .= ' AND ativo=true';
            }
            $b = Database::fetchOne('SELECT * FROM branding_municipio WHERE ' . $where . ' LIMIT 1', ['id' => $municipioId]);
            if (!$b) return null;
            return [
                'municipality_id' => $b['municipio_id'], 'municipio_id' => $b['municipio_id'],
                'system_name' => $b['nome_institucional'] ?? null, 'nome_institucional' => $b['nome_institucional'] ?? null,
                'logo_url' => UrlHelper::normalizeAssetUrl(($b['logo_url'] ?? null) ?: ($b['brasao_url'] ?? null)), 'brasao_url' => UrlHelper::normalizeAssetUrl($b['brasao_url'] ?? null),
                'favicon_url' => UrlHelper::normalizeAssetUrl($b['favicon_url'] ?? null), 'primary_color' => $b['cor_primaria_light'] ?? null,
                'secondary_color' => $b['cor_secundaria_light'] ?? null, 'accent_color' => $b['cor_accent_light'] ?? null,
                'cor_primaria_light' => $b['cor_primaria_light'] ?? null, 'cor_secundaria_light' => $b['cor_secundaria_light'] ?? null,
                'cor_accent_light' => $b['cor_accent_light'] ?? null, 'cor_primaria_dark' => $b['cor_primaria_dark'] ?? null,
                'cor_secundaria_dark' => $b['cor_secundaria_dark'] ?? null, 'cor_accent_dark' => $b['cor_accent_dark'] ?? null,
                'login_background_url' => UrlHelper::normalizeAssetUrl($b['login_bg_url'] ?? null), 'login_bg_url' => UrlHelper::normalizeAssetUrl($b['login_bg_url'] ?? null),
                'login_title' => $b['nome_institucional'] ?? null, 'login_subtitle' => $b['subtitulo'] ?? null,
                'mensagem_login' => $b['mensagem_login'] ?? null, 'modo_padrao' => $b['modo_padrao'] ?? 'light', 'footer_text' => $b['subtitulo'] ?? null,
            ];
        } catch (\Throwable) {
            return null;
        }
    }

    private function groups(?string $municipioId): array
    {
        if (!$municipioId || !$this->tableExists('grupos_trabalho') || !$this->hasColumn('grupos_trabalho', 'municipio_id')) {
            return [];
        }
        try {
            $where = 'municipio_id=:id';
            if ($this->hasColumn('grupos_trabalho', 'ativo')) {
                $where .= ' AND ativo=true';
            }
            $order = $this->hasColumn('grupos_trabalho', 'nome') ? ' ORDER BY nome' : '';
            return Database::fetchAll('SELECT * FROM grupos_trabalho WHERE ' . $where . $order, ['id' => $municipioId]);
        } catch (\Throwable) {
            return [];
        }
    }

    private function modules(?string $municipioId): array
    {
        if (!$this->tableExists('modulos')) {
            return [];
        }
        try {
            $where = $this->hasColumn('modulos', 'ativo') ? ' WHERE ativo=true' : '';
            $order = [];
            if ($this->hasColumn('modulos', 'ordem')) $order[] = 'ordem';
            if ($this->hasColumn('modulos', 'nome')) $order[] = 'nome';
            return Database::fetchAll('SELECT * FROM modulos' . $where . ($order ? ' ORDER BY ' . implode(', ', $order) : ''));
        } catch (\Throwable) {
            return [];
        }
    }

    private function permissions(string $userId, ?string $municipioId): array
    {
        if (!$this->tableExists('user_roles') || !$this->tableExists('role_permissoes') || !$this->tableExists('permissoes')) {
            return [];
        }
        if (!$this->hasColumn('user_roles', 'user_id') || !$this->hasColumn('user_roles', 'role')) {
            return [];
        }
        try {
            $whereMunicipio = $this->hasColumn('role_permissoes', 'municipio_id')
                ? ' AND (rp.municipio_id = :m OR rp.municipio_id IS NULL)'
                : '';
            $params = ['u' => $userId];
            if ($whereMunicipio) $params['m'] = $municipioId;
            $rows = Database::fetchAll(
                'SELECT DISTINCT p.codigo
                   FROM user_roles ur
                   JOIN role_permissoes rp ON rp.role = ur.role
                   JOIN permissoes p ON p.id = rp.permissao_id
                  WHERE ur.user_id = :u' . $whereMunicipio,
                $params
            );
            return array_column($rows, 'codigo');
        } catch (\Throwable) {
            return [];
        }
    }

    private function recordAttempt(string $email, Request $request, bool $ok): void
    {
        try {
            Database::execute('INSERT INTO tentativas_login (id,email,ip_address,user_agent,sucesso,ocorrido_em,created_at,updated_at) VALUES (:id,:email,:ip,:ua,:ok,:at,now(),now())', [
                'id' => $this->uuid(), 'email' => $email, 'ip' => $request->clientIp(), 'ua' => $request->userAgent(), 'ok' => $ok, 'at' => date(DATE_ATOM),
            ]);
        } catch (\Throwable) {}
    }

    private function recordSession(string $userId, ?string $municipioId, string $refresh, Request $request): void
    {
        try {
            Database::execute('INSERT INTO sessoes_login (id,user_id,municipio_id,refresh_token_hash,ip_address,user_agent,iniciada_em,created_at,updated_at) VALUES (:id,:u,:m,:h,:ip,:ua,:at,now(),now())', [
                'id' => $this->uuid(), 'u' => $userId, 'm' => $municipioId, 'h' => hash('sha256', $refresh), 'ip' => $request->clientIp(), 'ua' => $request->userAgent(), 'at' => date(DATE_ATOM),
            ]);
        } catch (\Throwable) {}
    }

    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);
        }
        @file_put_contents(
            $dir . DIRECTORY_SEPARATOR . 'api-error.log',
            '[' . date(DATE_ATOM) . '] ' . $context . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . ' body=' . json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL,
            FILE_APPEND
        );
    }

    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));
    }
}
