<?php

namespace App\Services;

/**
 * Servicio de autenticación con Keycloak
 * Maneja el flujo OAuth2 y sincronización de usuarios
 */
class KeycloakService
{
    private $config;
    private $database;

    public function __construct()
    {
        $this->config = require __DIR__ . '/../../config/keycloak.php';
        $this->database = Database::getInstance();
    }

    /**
     * Generar URL de autenticación de Keycloak con PKCE
     */
    public function getAuthUrl(string $state = null): string
    {
        // Generar code_verifier y code_challenge para PKCE
        $codeVerifier = $this->generateCodeVerifier();
        $codeChallenge = $this->generateCodeChallenge($codeVerifier);
        
        // Guardar code_verifier en sesión para usarlo después
        $_SESSION['oauth_code_verifier'] = $codeVerifier;
        
        $params = [
            'client_id' => $this->config['client_id'],
            'redirect_uri' => $this->config['redirect_uri'],
            'response_type' => 'code',
            'scope' => implode(' ', $this->config['scopes']),
            'code_challenge' => $codeChallenge,
            'code_challenge_method' => 'S256',
        ];

        if ($state) {
            $params['state'] = $state;
        }

        return $this->config['auth_url'] . '?' . http_build_query($params);
    }

    /**
     * Intercambiar código de autorización por token de acceso con PKCE
     */
    public function exchangeCodeForToken(string $code): ?array
    {
        $data = [
            'grant_type' => 'authorization_code',
            'client_id' => $this->config['client_id'],
            'client_secret' => $this->config['client_secret'],
            'code' => $code,
            'redirect_uri' => $this->config['redirect_uri'],
        ];
        
        // Agregar code_verifier para PKCE si está disponible
        if (isset($_SESSION['oauth_code_verifier'])) {
            $data['code_verifier'] = $_SESSION['oauth_code_verifier'];
            // Limpiar el code_verifier de la sesión
            unset($_SESSION['oauth_code_verifier']);
        }

        $response = $this->makeHttpRequest($this->config['token_url'], $data);
        
        if ($response && isset($response['access_token'])) {
            return $response;
        }

        return null;
    }

    /**
     * Obtener información del usuario desde Keycloak
     */
    public function getUserInfo(string $accessToken): ?array
    {
        $headers = [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json',
        ];

        return $this->makeHttpRequest($this->config['userinfo_url'], null, $headers);
    }

    /**
     * Sincronizar usuario de Keycloak con base de datos local
     */
    public function syncUser(array $keycloakUser): ?array
    {
        // 1. Buscar usuario existente por keycloak_id
        $existingUser = $this->database->fetchOne(
            'SELECT * FROM users WHERE keycloak_id = ?',
            [$keycloakUser['sub']]
        );

        if ($existingUser) {
            // Usuario ya sincronizado - actualizar información
            $this->database->update(
                'UPDATE users SET 
                    username = ?, 
                    email = ?, 
                    first_name = ?, 
                    last_name = ?, 
                    last_login = NOW(),
                    updated_at = NOW() 
                WHERE keycloak_id = ?',
                [
                    $keycloakUser['preferred_username'] ?? $keycloakUser['email'],
                    $keycloakUser['email'],
                    $keycloakUser['given_name'] ?? '',
                    $keycloakUser['family_name'] ?? '',
                    $keycloakUser['sub']
                ]
            );

            // Obtener usuario actualizado
            return $this->database->fetchOne(
                'SELECT u.*, r.name as role_name, r.display_name as role_display_name, r.is_admin, r.permissions
                FROM users u 
                LEFT JOIN roles r ON u.role_id = r.id 
                WHERE u.keycloak_id = ?',
                [$keycloakUser['sub']]
            );
        }

        // 2. Buscar si fue invitado previamente (existe por email pero sin keycloak_id)
        $invitedUser = $this->database->fetchOne(
            'SELECT * FROM users WHERE email = ? AND keycloak_id IS NULL',
            [$keycloakUser['email']]
        );

        if ($invitedUser) {
            // Usuario invitado que ahora se loguea - actualizar con keycloak_id
            $this->database->update(
                'UPDATE users SET 
                    keycloak_id = ?,
                    username = ?, 
                    first_name = ?, 
                    last_name = ?, 
                    is_active = TRUE,
                    last_login = NOW(),
                    updated_at = NOW(),
                    invitation_token = NULL,
                    invitation_expiry = NULL
                WHERE id = ?',
                [
                    $keycloakUser['sub'],
                    $keycloakUser['preferred_username'] ?? $keycloakUser['email'],
                    $keycloakUser['given_name'] ?? '',
                    $keycloakUser['family_name'] ?? '',
                    $invitedUser['id']
                ]
            );

            // Registrar activación
            $this->logAuthAction($invitedUser['id'], 'user_activated', [
                'keycloak_id' => $keycloakUser['sub'],
                'email' => $keycloakUser['email']
            ]);

            // Obtener usuario actualizado
            return $this->database->fetchOne(
                'SELECT u.*, r.name as role_name, r.display_name as role_display_name, r.is_admin, r.permissions
                FROM users u 
                LEFT JOIN roles r ON u.role_id = r.id 
                WHERE u.id = ?',
                [$invitedUser['id']]
            );
        }

        // 3. Usuario completamente nuevo - crear
        $userId = $this->database->insert(
            'INSERT INTO users (keycloak_id, username, email, first_name, last_name, is_active, last_login, created_at, updated_at) 
            VALUES (?, ?, ?, ?, ?, FALSE, NOW(), NOW(), NOW())',
            [
                $keycloakUser['sub'],
                $keycloakUser['preferred_username'] ?? $keycloakUser['email'],
                $keycloakUser['email'],
                $keycloakUser['given_name'] ?? '',
                $keycloakUser['family_name'] ?? ''
            ]
        );

        // Registrar log de nuevo usuario
        $this->logAuthAction($userId, 'user_created', [
            'keycloak_id' => $keycloakUser['sub'],
            'email' => $keycloakUser['email']
        ]);

        // Obtener usuario recién creado
        return $this->database->fetchOne(
            'SELECT u.*, r.name as role_name, r.display_name as role_display_name, r.is_admin, r.permissions
            FROM users u 
            LEFT JOIN roles r ON u.role_id = r.id 
            WHERE u.id = ?',
            [$userId]
        );
    }

    /**
     * Verificar si el usuario tiene acceso al sistema
     */
    public function userHasAccess(array $user): bool
    {
        $hasRoleId = isset($user['role_id']) && $user['role_id'] !== null && $user['role_id'] !== '';
        $isActive = !empty($user['is_active']) && ($user['is_active'] == 1 || $user['is_active'] === true || $user['is_active'] === 'true');
        
        error_log('KeycloakService::userHasAccess() - hasRoleId=' . ($hasRoleId ? 'true' : 'false') . ', isActive=' . ($isActive ? 'true' : 'false'));
        
        return $hasRoleId && $isActive;
    }

    /**
     * Registrar acción de autenticación
     */
    public function logAuthAction(int $userId, string $action, array $details = []): void
    {
        $this->database->insert(
            'INSERT INTO auth_logs (user_id, action, ip_address, user_agent, details, created_at) 
            VALUES (?, ?, ?, ?, ?, NOW())',
            [
                $userId,
                $action,
                $_SERVER['REMOTE_ADDR'] ?? null,
                $_SERVER['HTTP_USER_AGENT'] ?? null,
                json_encode($details)
            ]
        );
    }

    /**
     * Generar URL de logout de Keycloak
     */
    public function getLogoutUrl(string $redirectUri = null): string
    {
        $params = [];
        if ($redirectUri) {
            $params['redirect_uri'] = $redirectUri;
        }

        $queryString = !empty($params) ? '?' . http_build_query($params) : '';
        return $this->config['logout_url'] . $queryString;
    }

    /**
     * Realizar petición HTTP
     */
    private function makeHttpRequest(string $url, array $data = null, array $headers = []): ?array
    {
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER => $headers,
        ]);

        if ($data !== null) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode >= 200 && $httpCode < 300) {
            return json_decode($response, true);
        }

        error_log("Keycloak API Error: HTTP $httpCode - $response");
        return null;
    }

    /**
     * Generar code_verifier para PKCE
     */
    private function generateCodeVerifier(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
    }

    /**
     * Generar code_challenge para PKCE
     */
    private function generateCodeChallenge(string $codeVerifier): string
    {
        return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
    }

    // ==================== ADMIN API METHODS ====================

    /**
     * Obtener token de admin para Admin API
     */
    private function getAdminToken(): string
    {
        $adminUser = $_ENV['KEYCLOAK_ADMIN_USER'] ?? '';
        $adminPassword = $_ENV['KEYCLOAK_ADMIN_PASSWORD'] ?? '';
        
        if (empty($adminUser) || empty($adminPassword)) {
            throw new \Exception('Credenciales de admin no configuradas');
        }

        $url = $this->config['token_url'];
        
        $data = [
            'grant_type' => 'password',
            'client_id' => 'admin-cli',
            'username' => $adminUser,
            'password' => $adminPassword
        ];

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode !== 200) {
            error_log("Keycloak admin token error: {$response}");
            throw new \Exception('No se pudo obtener token de admin');
        }

        $result = json_decode($response, true);
        return $result['access_token'] ?? '';
    }

    /**
     * Buscar usuario por email
     */
    private function findUserByEmail(string $email, string $token): ?array
    {
        $baseUrl = $this->config['base_url'] ?? str_replace('/protocol/openid-connect', '', dirname($this->config['token_url']));
        $realm = $this->config['realm'] ?? 'master';
        
        $url = "{$baseUrl}/admin/realms/{$realm}/users?email=" . urlencode($email) . "&exact=true";

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer {$token}"]);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            return null;
        }

        $users = json_decode($response, true);
        return !empty($users) ? $users[0] : null;
    }

    /**
     * Crear usuario en Keycloak
     */
    private function createUserInKeycloak(string $email, string $firstName, string $lastName, string $token): string
    {
        $baseUrl = $this->config['base_url'] ?? str_replace('/protocol/openid-connect', '', dirname($this->config['token_url']));
        $realm = $this->config['realm'] ?? 'master';
        
        $url = "{$baseUrl}/admin/realms/{$realm}/users";

        $username = strtolower(explode('@', $email)[0]);

        $userData = [
            'username' => $username,
            'email' => $email,
            'firstName' => $firstName,
            'lastName' => $lastName,
            'enabled' => true,
            'emailVerified' => false,
            'requiredActions' => ['VERIFY_EMAIL', 'UPDATE_PASSWORD']
        ];

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($userData));
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            "Authorization: Bearer {$token}"
        ]);
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $headers = substr($response, 0, $headerSize);
        curl_close($ch);

        if ($httpCode !== 201) {
            throw new \Exception('Error creando usuario en Keycloak');
        }

        preg_match('/Location: .+\/users\/([a-f0-9\-]+)/i', $headers, $matches);
        return $matches[1] ?? '';
    }

    /**
     * Enviar email de verificación
     */
    private function sendVerificationEmail(string $userId, string $token): void
    {
        $baseUrl = $this->config['base_url'] ?? str_replace('/protocol/openid-connect', '', dirname($this->config['token_url']));
        $realm = $this->config['realm'] ?? 'master';
        $clientId = $this->config['client_id'];
        
        $url = "{$baseUrl}/admin/realms/{$realm}/users/{$userId}/send-verify-email?client_id=" . urlencode($clientId);

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            "Authorization: Bearer {$token}"
        ]);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 204 && $httpCode !== 200) {
            error_log("Error enviando email: {$response}");
            throw new \Exception('Error enviando correo');
        }
    }

    /**
     * Asignar roles del client al usuario
     */
    private function assignClientRoles(string $userId, string $token): void
    {
        $baseUrl = $this->config['base_url'] ?? str_replace('/protocol/openid-connect', '', dirname($this->config['token_url']));
        $realm = $this->config['realm'] ?? 'master';
        
        $clientUUID = $this->getClientUUID($token);
        if (!$clientUUID) return;

        $roles = $this->getClientRoles($clientUUID, $token);
        if (empty($roles)) return;

        $rolesToAssign = [];
        foreach ($roles as $role) {
            if (in_array($role['name'], ['user', 'default', 'app-user'])) {
                $rolesToAssign[] = $role;
                break;
            }
        }

        if (empty($rolesToAssign) && !empty($roles)) {
            $rolesToAssign[] = $roles[0];
        }

        if (empty($rolesToAssign)) return;

        $url = "{$baseUrl}/admin/realms/{$realm}/users/{$userId}/role-mappings/clients/{$clientUUID}";

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($rolesToAssign));
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            "Authorization: Bearer {$token}"
        ]);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        curl_exec($ch);
        curl_close($ch);
    }

    /**
     * Obtener UUID del client
     */
    private function getClientUUID(string $token): ?string
    {
        $baseUrl = $this->config['base_url'] ?? str_replace('/protocol/openid-connect', '', dirname($this->config['token_url']));
        $realm = $this->config['realm'] ?? 'master';
        $clientId = $this->config['client_id'];
        
        $url = "{$baseUrl}/admin/realms/{$realm}/clients?clientId=" . urlencode($clientId);

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer {$token}"]);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        $response = curl_exec($ch);
        curl_close($ch);

        $clients = json_decode($response, true);
        return $clients[0]['id'] ?? null;
    }

    /**
     * Obtener roles del client
     */
    private function getClientRoles(string $clientUUID, string $token): array
    {
        $baseUrl = $this->config['base_url'] ?? str_replace('/protocol/openid-connect', '', dirname($this->config['token_url']));
        $realm = $this->config['realm'] ?? 'master';
        
        $url = "{$baseUrl}/admin/realms/{$realm}/clients/{$clientUUID}/roles";

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer {$token}"]);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        
        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true) ?? [];
    }

    /**
     * Invitar usuario - Si existe le da acceso, si no existe lo crea
     */
    public function inviteUser(string $email, string $firstName, string $lastName): array
    {
        try {
            $token = $this->getAdminToken();
            $existingUser = $this->findUserByEmail($email, $token);

            if ($existingUser) {
                $userId = $existingUser['id'];
                $emailVerified = $existingUser['emailVerified'] ?? false;

                $this->assignClientRoles($userId, $token);

                if (!$emailVerified) {
                    $this->sendVerificationEmail($userId, $token);
                    $message = '✅ Usuario existente. Acceso otorgado. Correo de verificación re-enviado.';
                } else {
                    $message = '✅ Usuario existente. Acceso otorgado a la aplicación.';
                }

                return [
                    'keycloak_id' => $userId,
                    'username' => $existingUser['username'],
                    'existed' => true,
                    'message' => $message,
                    'success' => true
                ];
            } else {
                $userId = $this->createUserInKeycloak($email, $firstName, $lastName, $token);
                
                if (empty($userId)) {
                    throw new \Exception('No se pudo crear usuario');
                }

                $this->assignClientRoles($userId, $token);
                $this->sendVerificationEmail($userId, $token);

                return [
                    'keycloak_id' => $userId,
                    'username' => strtolower(explode('@', $email)[0]),
                    'existed' => false,
                    'message' => '✅ Usuario creado. Correo de verificación enviado a ' . $email,
                    'success' => true
                ];
            }
        } catch (\Exception $e) {
            error_log("KeycloakService::inviteUser error: " . $e->getMessage());
            throw $e;
        }
    }

    /**
     * Verificar si Admin API está disponible
     */
    public function isAdminApiAvailable(): bool
    {
        try {
            $token = $this->getAdminToken();
            return !empty($token);
        } catch (\Exception $e) {
            return false;
        }
    }
}
