<?php
namespace App\Services;

use App\Core\Auth;
use App\Core\Database;
use App\Core\Security;
use PDO;
use Throwable;

final class OAuthLoginService
{
    private const PROVIDERS = ['google', 'microsoft'];

    public function start(string $provider): void
    {
        $provider = $this->normalizeProvider($provider);
        $cfg = $this->providerConfig($provider);
        $this->assertProviderConfigured($provider, $cfg);

        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        $state = bin2hex(random_bytes(24));
        $_SESSION['oauth_state_' . $provider] = $state;
        $_SESSION['oauth_state_created_' . $provider] = time();

        $params = [
            'client_id' => $cfg['client_id'],
            'redirect_uri' => $this->redirectUri($provider, $cfg),
            'response_type' => 'code',
            'scope' => 'openid email profile',
            'state' => $state,
            'prompt' => 'select_account',
        ];

        $url = $provider === 'google'
            ? 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query($params)
            : 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' . http_build_query($params);

        header('Location: ' . $url, true, 302);
        exit;
    }

    public function callback(string $provider, array $query): void
    {
        $provider = $this->normalizeProvider($provider);
        $cfg = $this->providerConfig($provider);
        $this->assertProviderConfigured($provider, $cfg);

        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        if (!empty($query['error'])) {
            $this->fail('Sign-in was cancelled or rejected by the provider. Please try again.');
        }

        $code = trim((string)($query['code'] ?? ''));
        $state = trim((string)($query['state'] ?? ''));
        $expected = (string)($_SESSION['oauth_state_' . $provider] ?? '');
        $createdAt = (int)($_SESSION['oauth_state_created_' . $provider] ?? 0);

        unset($_SESSION['oauth_state_' . $provider], $_SESSION['oauth_state_created_' . $provider]);

        if ($code === '' || $state === '' || $expected === '' || !hash_equals($expected, $state)) {
            $this->fail('Secure sign-in could not be verified. Please try again.');
        }

        if ($createdAt < time() - 600) {
            $this->fail('Secure sign-in expired. Please try again.');
        }

        $tokens = $this->exchangeCodeForToken($provider, $cfg, $code);
        $profile = $this->fetchProfile($provider, (string)($tokens['access_token'] ?? ''));

        $email = strtolower(trim((string)($profile['email'] ?? $profile['preferred_username'] ?? '')));
        $name = trim((string)($profile['name'] ?? ''));
        $subject = trim((string)($profile['sub'] ?? ''));
        $providerVerified = $this->providerEmailVerified($provider, $profile);

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->fail('The provider did not return a valid email address. Please use email/password signup.');
        }

        if (!$providerVerified) {
            $label = $provider === 'google' ? 'Google' : 'Microsoft / Outlook';
            $this->fail('Your ' . $label . ' email is not verified. Please verify it with the provider first.');
        }

        $pdo = Database::connection();
        $user = $this->findUserByEmail($pdo, $email);
        $createdNewMember = false;

        if (!$user) {
            if (!$this->socialSignupEnabled()) {
                $this->fail('No Pyrk account exists for ' . htmlspecialchars($email, ENT_QUOTES, 'UTF-8') . '. Please create an account first.');
            }
            $user = $this->createMemberFromOAuth($pdo, $provider, $subject, $email, $name);
            $createdNewMember = true;
        } else {
            $status = (string)($user['status'] ?? '');
            if ($status === 'blocked') {
                $this->fail('This Pyrk account is blocked. Please contact the administrator.');
            }

            // If the only reason the existing account is inactive is email activation, verified Google/Microsoft login can activate it safely.
            if ($status === 'inactive') {
                if (empty($user['email_verified_at'])) {
                    $pdo->prepare('UPDATE users SET status = "active", email_verified_at = COALESCE(email_verified_at, NOW()), activated_at = COALESCE(activated_at, NOW()), activation_token = NULL, activation_expires_at = NULL, activation_sent_at = NULL, updated_at = NOW() WHERE id = ?')
                        ->execute([(int)$user['id']]);
                    $user['status'] = 'active';
                    $user['email_verified_at'] = date('Y-m-d H:i:s');
                } else {
                    $this->fail('This Pyrk account is inactive. Please contact the administrator.');
                }
            }
        }

        $this->bestEffortRememberProvider((int)$user['id'], $provider, $subject, $name, $createdNewMember);

        if (!Auth::loginById((int)$user['id'])) {
            $this->fail('Could not create a Pyrk session. Please use email/password login.');
        }

        Security::audit((int)$user['id'], $createdNewMember ? 'oauth_signup_' . $provider : 'oauth_login_' . $provider, 'users', (int)$user['id'], [
            'email' => $email,
            'provider' => $provider,
            'created_new_member' => $createdNewMember,
        ]);

        $userAfterLogin = Auth::user();
        $role = (string)($userAfterLogin['role_name'] ?? $user['role_name'] ?? 'member');
        $redirect = in_array($role, ['super_admin', 'event_manager', 'walk_leader'], true)
            ? 'admin/dashboard.php'
            : (Auth::isNativeAppRequest() ? 'mobile_home.php' : 'index.php');

        header('Location: ' . $redirect, true, 302);
        exit;
    }

    private function normalizeProvider(string $provider): string
    {
        $provider = strtolower(trim($provider));
        if ($provider === 'outlook') {
            $provider = 'microsoft';
        }
        if (!in_array($provider, self::PROVIDERS, true)) {
            $this->fail('Unsupported sign-in provider.');
        }
        return $provider;
    }

    private function providerConfig(string $provider): array
    {
        return (array)($GLOBALS['config']['oauth'][$provider] ?? []);
    }

    private function assertProviderConfigured(string $provider, array $cfg): void
    {
        if (empty($cfg['enabled']) || empty($cfg['client_id']) || empty($cfg['client_secret'])) {
            $label = $provider === 'google' ? 'Google' : 'Microsoft / Outlook';
            $this->fail($label . ' signup/login is not configured yet. Please add the OAuth client ID and secret in config/app.php.');
        }
    }

    private function socialSignupEnabled(): bool
    {
        $oauth = (array)($GLOBALS['config']['oauth'] ?? []);
        if (array_key_exists('auto_create_members', $oauth)) {
            return filter_var($oauth['auto_create_members'], FILTER_VALIDATE_BOOL);
        }
        return true;
    }

    private function redirectUri(string $provider, array $cfg): string
    {
        if (!empty($cfg['redirect_uri'])) {
            return (string)$cfg['redirect_uri'];
        }

        $base = rtrim((string)($GLOBALS['config']['base_url'] ?? ''), '/');
        if ($base !== '') {
            return $base . '/oauth.php?provider=' . rawurlencode($provider);
        }

        $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
        $scheme = $https ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
        $path = strtok($_SERVER['REQUEST_URI'] ?? '/steps/public/oauth.php', '?');
        return $scheme . '://' . $host . $path . '?provider=' . rawurlencode($provider);
    }

    private function exchangeCodeForToken(string $provider, array $cfg, string $code): array
    {
        $url = $provider === 'google'
            ? 'https://oauth2.googleapis.com/token'
            : 'https://login.microsoftonline.com/common/oauth2/v2.0/token';

        $data = [
            'client_id' => $cfg['client_id'],
            'client_secret' => $cfg['client_secret'],
            'code' => $code,
            'redirect_uri' => $this->redirectUri($provider, $cfg),
            'grant_type' => 'authorization_code',
        ];

        return $this->httpPostForm($url, $data);
    }

    private function fetchProfile(string $provider, string $accessToken): array
    {
        if ($accessToken === '') {
            $this->fail('The provider did not return an access token. Please try again.');
        }

        $url = $provider === 'google'
            ? 'https://openidconnect.googleapis.com/v1/userinfo'
            : 'https://graph.microsoft.com/oidc/userinfo';

        return $this->httpGetJson($url, [
            'Authorization: Bearer ' . $accessToken,
        ]);
    }

    private function providerEmailVerified(string $provider, array $profile): bool
    {
        if ($provider === 'google' && array_key_exists('email_verified', $profile)) {
            return filter_var($profile['email_verified'], FILTER_VALIDATE_BOOL);
        }

        // Microsoft OIDC userinfo normally returns the account email after successful Microsoft authentication.
        // If an email/preferred_username was returned by Microsoft, treat it as verified for account creation/login.
        if ($provider === 'microsoft') {
            return !empty($profile['email']) || !empty($profile['preferred_username']);
        }

        return true;
    }

    private function findUserByEmail(PDO $pdo, string $email): ?array
    {
        $stmt = $pdo->prepare('SELECT u.id, u.full_name, u.email, u.status, u.email_verified_at, r.name AS role_name
                               FROM users u
                               JOIN roles r ON r.id = u.role_id
                               WHERE LOWER(u.email) = ?
                               LIMIT 1');
        $stmt->execute([$email]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        return $user ?: null;
    }

    private function createMemberFromOAuth(PDO $pdo, string $provider, string $subject, string $email, string $name): array
    {
        $roleId = (int)$pdo->query('SELECT id FROM roles WHERE name = "member" LIMIT 1')->fetchColumn();
        if (!$roleId) {
            $this->fail('Member role is missing. Please contact the administrator.');
        }

        $displayName = $this->safeDisplayName($name, $email);
        $qrToken = 'PYRK-USER-' . strtoupper(bin2hex(random_bytes(8)));
        $randomPasswordHash = password_hash(bin2hex(random_bytes(32)), PASSWORD_DEFAULT);
        $columns = $this->tableColumns($pdo, 'users');

        $fields = [
            'role_id' => $roleId,
            'full_name' => $displayName,
            'email' => $email,
            'mobile' => null,
            'qr_token' => $qrToken,
            'password_hash' => $randomPasswordHash,
            'status' => 'active',
            'email_verified_at' => date('Y-m-d H:i:s'),
            'activation_token' => null,
            'activation_expires_at' => null,
            'activation_sent_at' => null,
            'activated_at' => date('Y-m-d H:i:s'),
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s'),
        ];

        $optional = [
            'oauth_provider' => $provider,
            'oauth_subject' => $subject !== '' ? $subject : null,
            'oauth_name' => $name !== '' ? $name : null,
            'oauth_registered_at' => date('Y-m-d H:i:s'),
            'signup_method' => $provider,
        ];
        foreach ($optional as $column => $value) {
            if (in_array($column, $columns, true)) {
                $fields[$column] = $value;
            }
        }

        $insertFields = [];
        $placeholders = [];
        $values = [];
        foreach ($fields as $column => $value) {
            if (in_array($column, $columns, true)) {
                $insertFields[] = '`' . str_replace('`', '``', $column) . '`';
                $placeholders[] = '?';
                $values[] = $value;
            }
        }

        $pdo->beginTransaction();
        try {
            $sql = 'INSERT INTO users (' . implode(', ', $insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')';
            $pdo->prepare($sql)->execute($values);
            $userId = (int)$pdo->lastInsertId();

            $profileColumns = $this->tableColumns($pdo, 'member_profiles');
            if (in_array('user_id', $profileColumns, true)) {
                $profileFields = [
                    'user_id' => $userId,
                    'date_of_birth' => null,
                    'gender' => 'prefer_not_to_say',
                    'emergency_contact_name' => null,
                    'emergency_contact_mobile' => null,
                    'medical_conditions' => null,
                    'photo_consent' => 0,
                    'created_at' => date('Y-m-d H:i:s'),
                    'updated_at' => date('Y-m-d H:i:s'),
                ];
                $pf = [];
                $pp = [];
                $pv = [];
                foreach ($profileFields as $column => $value) {
                    if (in_array($column, $profileColumns, true)) {
                        $pf[] = '`' . str_replace('`', '``', $column) . '`';
                        $pp[] = '?';
                        $pv[] = $value;
                    }
                }
                $pdo->prepare('INSERT INTO member_profiles (' . implode(', ', $pf) . ') VALUES (' . implode(', ', $pp) . ')')->execute($pv);
            }

            $pdo->commit();

            try {
                (new NotificationService())->notifyMemberRegistered($userId);
            } catch (Throwable) {
                // Signup must not fail because admin email notification failed.
            }

            return [
                'id' => $userId,
                'full_name' => $displayName,
                'email' => $email,
                'status' => 'active',
                'email_verified_at' => date('Y-m-d H:i:s'),
                'role_name' => 'member',
            ];
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            if (str_contains($e->getMessage(), 'Duplicate')) {
                $this->fail('This email already exists. Please try logging in again.');
            }
            $this->fail('Could not create your Pyrk account from ' . ($provider === 'google' ? 'Google' : 'Outlook') . '. Please use the normal signup form or contact the administrator.');
        }
    }

    private function safeDisplayName(string $name, string $email): string
    {
        $name = trim(preg_replace('/\s+/', ' ', $name));
        if ($name !== '') {
            return mb_substr($name, 0, 150);
        }

        $local = preg_replace('/[^a-zA-Z0-9._-]+/', ' ', strtok($email, '@') ?: 'Pyrk Member');
        $local = trim(str_replace(['.', '_', '-'], ' ', (string)$local));
        $local = $local !== '' ? ucwords(strtolower($local)) : 'Pyrk Member';
        return mb_substr($local, 0, 150);
    }

    private function tableColumns(PDO $pdo, string $table): array
    {
        try {
            return $pdo->query('SHOW COLUMNS FROM `' . str_replace('`', '``', $table) . '`')->fetchAll(PDO::FETCH_COLUMN);
        } catch (Throwable) {
            return [];
        }
    }

    private function httpPostForm(string $url, array $data): array
    {
        if (!function_exists('curl_init')) {
            $this->fail('PHP cURL is required for social signup/login. Please enable cURL on the server.');
        }

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($data),
            CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_TIMEOUT => 20,
        ]);
        $body = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($body === false || $status < 200 || $status >= 300) {
            $this->fail('The signup provider rejected the request. Please check client ID, secret and redirect URI. ' . ($error ? 'cURL: ' . $error : ''));
        }

        $json = json_decode((string)$body, true);
        if (!is_array($json)) {
            $this->fail('The signup provider returned an invalid response.');
        }
        return $json;
    }

    private function httpGetJson(string $url, array $headers): array
    {
        if (!function_exists('curl_init')) {
            $this->fail('PHP cURL is required for social signup/login. Please enable cURL on the server.');
        }

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPGET => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 20,
        ]);
        $body = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($body === false || $status < 200 || $status >= 300) {
            $this->fail('Could not read your profile from the signup provider. ' . ($error ? 'cURL: ' . $error : ''));
        }

        $json = json_decode((string)$body, true);
        if (!is_array($json)) {
            $this->fail('The signup provider returned an invalid profile response.');
        }
        return $json;
    }

    private function bestEffortRememberProvider(int $userId, string $provider, string $subject, string $name, bool $isSignup = false): void
    {
        try {
            $pdo = Database::connection();
            $columns = $this->tableColumns($pdo, 'users');
            $sets = [];
            $values = [];

            if (in_array('oauth_provider', $columns, true)) {
                $sets[] = 'oauth_provider = ?';
                $values[] = $provider;
            }
            if (in_array('oauth_subject', $columns, true) && $subject !== '') {
                $sets[] = 'oauth_subject = ?';
                $values[] = $subject;
            }
            if (in_array('oauth_name', $columns, true) && $name !== '') {
                $sets[] = 'oauth_name = ?';
                $values[] = $name;
            }
            if ($isSignup && in_array('oauth_registered_at', $columns, true)) {
                $sets[] = 'oauth_registered_at = COALESCE(oauth_registered_at, NOW())';
            }
            if (in_array('signup_method', $columns, true) && $isSignup) {
                $sets[] = 'signup_method = ?';
                $values[] = $provider;
            }

            if ($sets) {
                $sets[] = 'updated_at = NOW()';
                $values[] = $userId;
                $pdo->prepare('UPDATE users SET ' . implode(', ', $sets) . ' WHERE id = ?')->execute($values);
            }
        } catch (Throwable) {
            // OAuth tracking must never break signup/login.
        }
    }

    private function fail(string $message): void
    {
        if (!headers_sent()) {
            http_response_code(400);
        }
        $safeMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
        echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Signup issue | Pyrk</title><link rel="stylesheet" href="assets/css/app.css"><link rel="stylesheet" href="assets/css/social_login_pro.css?v=2"></head><body class="auth-page"><main class="oauth-error-card"><span>PYRK</span><h1>Signup/login issue</h1><p>' . $safeMessage . '</p><a class="btn" href="login-dev.php">Back to login</a></main></body></html>';
        exit;
    }
}
