<?php

namespace Calio\Core;

use Calio\Support\Settings;

if (!defined('ABSPATH')) {
    exit;
}

final class GoogleCalendarOAuthManager
{
    public const ACTION_START = 'calio_google_oauth_start';
    public const ACTION_CALLBACK = 'calio_google_oauth_callback';
    public const ACTION_DISCONNECT = 'calio_google_oauth_disconnect';
    public const ACTION_TEST = 'calio_google_oauth_test';
    public const ACTION_REFRESH = 'calio_google_oauth_refresh';

    private const STATE_TRANSIENT_PREFIX = 'calio_google_oauth_state_';
    private const TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
    private const AUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth';
    private const REVOKE_ENDPOINT = 'https://oauth2.googleapis.com/revoke';
    private const CALENDAR_LIST_ENDPOINT = 'https://www.googleapis.com/calendar/v3/users/me/calendarList';
    private const USERINFO_ENDPOINT = 'https://openidconnect.googleapis.com/v1/userinfo';
    private const NONCE_START = 'calio_google_oauth_start';
    private const NONCE_DISCONNECT = 'calio_google_oauth_disconnect';
    private const NONCE_TEST = 'calio_google_oauth_test';
    private const NONCE_REFRESH = 'calio_google_oauth_refresh';
    private const CLOCK_SKEW = 90;

    /**
     * @return array<int, string>
     */
    private static function scopes(): array
    {
        return [
            'openid',
            'email',
            'profile',
            'https://www.googleapis.com/auth/calendar.readonly',
            'https://www.googleapis.com/auth/calendar.calendarlist.readonly',
        ];
    }

    public function __construct()
    {
        add_filter('allowed_redirect_hosts', [self::class, 'allow_google_redirect_host']);

        add_action('admin_post_' . self::ACTION_START, [$this, 'handle_connect_request']);
        add_action('admin_post_' . self::ACTION_CALLBACK, [$this, 'handle_callback_request']);
        add_action('admin_post_' . self::ACTION_DISCONNECT, [$this, 'handle_disconnect_request']);
        add_action('admin_post_' . self::ACTION_TEST, [$this, 'handle_test_request']);
        add_action('admin_post_' . self::ACTION_REFRESH, [$this, 'handle_refresh_request']);
    }

    /**
     * Allows the OAuth consent redirect while keeping `wp_safe_redirect()` in use.
     *
     * @param array<int, string> $hosts WordPress safe redirect host allowlist.
     * @return array<int, string>
     */
    public static function allow_google_redirect_host(array $hosts): array
    {
        $hosts[] = 'accounts.google.com';

        return array_values(array_unique($hosts));
    }

    public static function redirect_uri(): string
    {
        return admin_url('admin-post.php?action=' . self::ACTION_CALLBACK);
    }

    public static function settings_url(): string
    {
        return admin_url('admin.php?page=' . CALIO_MENU_SLUG . '#calio_addon_api_settings');
    }

    public static function start_url(): string
    {
        return admin_url('admin-post.php?action=' . self::ACTION_START);
    }

    public static function disconnect_url(): string
    {
        return admin_url('admin-post.php?action=' . self::ACTION_DISCONNECT);
    }

    public static function test_url(): string
    {
        return admin_url('admin-post.php?action=' . self::ACTION_TEST);
    }

    public static function refresh_url(): string
    {
        return admin_url('admin-post.php?action=' . self::ACTION_REFRESH);
    }

    public static function has_client_credentials(): bool
    {
        $settings = Settings::api_settings();

        return trim((string) ($settings['google_calendar_client_id'] ?? '')) !== ''
            && trim((string) ($settings['google_calendar_client_secret'] ?? '')) !== '';
    }

    public static function is_connected(): bool
    {
        $settings = Settings::api_settings();

        return trim((string) ($settings['google_calendar_refresh_token'] ?? '')) !== ''
            || trim((string) ($settings['google_calendar_access_token'] ?? '')) !== '';
    }

    /**
     * @return array<int, array{id: string, summary: string, primary: bool, color: string, access_role: string, time_zone: string}>
     */
    public static function cached_calendars(): array
    {
        $settings = Settings::api_settings();
        $items = is_array($settings['google_calendar_calendar_cache'] ?? null) ? $settings['google_calendar_calendar_cache'] : [];

        return array_values(array_filter(array_map(
            static function ($item): array {
                if (!is_array($item)) {
                    return [];
                }

                $id = sanitize_text_field((string) ($item['id'] ?? ''));
                $summary = sanitize_text_field((string) ($item['summary'] ?? ''));
                if ($id === '' || $summary === '') {
                    return [];
                }

                return [
                    'id' => $id,
                    'summary' => $summary,
                    'primary' => !empty($item['primary']),
                    'color' => sanitize_hex_color((string) ($item['color'] ?? '')) ?: '',
                    'access_role' => sanitize_key((string) ($item['access_role'] ?? '')),
                    'time_zone' => sanitize_text_field((string) ($item['time_zone'] ?? '')),
                ];
            },
            $items
        )));
    }

    /**
     * @return array<string, string>
     */
    public static function cached_calendar_options(): array
    {
        $options = [];

        foreach (self::cached_calendars() as $calendar) {
            $options[$calendar['id']] = $calendar['summary'] . ($calendar['primary'] ? ' (' . esc_html__('Primary', 'calio-addon') . ')' : '');
        }

        return $options;
    }

    public static function get_valid_access_token(): string
    {
        $settings = Settings::api_settings();
        $accessToken = trim((string) ($settings['google_calendar_access_token'] ?? ''));
        $expiresAt = (int) ($settings['google_calendar_token_expires_at'] ?? 0);

        if ($accessToken !== '' && $expiresAt > (time() + self::CLOCK_SKEW)) {
            return $accessToken;
        }

        $refreshToken = trim((string) ($settings['google_calendar_refresh_token'] ?? ''));
        if ($refreshToken === '' || !self::has_client_credentials()) {
            return '';
        }

        return self::refresh_access_token($settings);
    }

    public function handle_connect_request(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You are not allowed to connect Google Calendar.', 'calio-addon'));
        }

        check_admin_referer(self::NONCE_START);

        $settings = Settings::api_settings();
        $clientId = trim((string) ($settings['google_calendar_client_id'] ?? ''));
        $clientSecret = trim((string) ($settings['google_calendar_client_secret'] ?? ''));

        if ($clientId === '' || $clientSecret === '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Save the Google Client ID and Client Secret before connecting.', 'calio-addon'),
            ]);
            $this->redirect_to_settings();
        }

        $state = wp_generate_password(40, false, false);
        set_transient(
            self::STATE_TRANSIENT_PREFIX . $state,
            [
                'user_id' => get_current_user_id(),
                'created_at' => time(),
            ],
            15 * MINUTE_IN_SECONDS
        );

        $query = [
            'client_id' => $clientId,
            'redirect_uri' => self::redirect_uri(),
            'response_type' => 'code',
            'scope' => implode(' ', self::scopes()),
            'access_type' => 'offline',
            'include_granted_scopes' => 'true',
            'prompt' => 'consent',
            'state' => $state,
        ];

        $loginHint = trim((string) ($settings['google_calendar_connected_email'] ?? ''));
        if ($loginHint !== '') {
            $query['login_hint'] = $loginHint;
        }

        wp_safe_redirect(add_query_arg($query, self::AUTH_ENDPOINT));
        exit;
    }

    public function handle_callback_request(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You are not allowed to finish Google Calendar connection.', 'calio-addon'));
        }

        // Google OAuth callback is validated through the transient-backed state token.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $state = isset($_GET['state']) ? sanitize_text_field((string) wp_unslash($_GET['state'])) : '';
        $statePayload = $state !== '' ? get_transient(self::STATE_TRANSIENT_PREFIX . $state) : false;

        if ($state !== '') {
            delete_transient(self::STATE_TRANSIENT_PREFIX . $state);
        }

        if (!is_array($statePayload) || ((int) ($statePayload['user_id'] ?? 0)) !== get_current_user_id()) {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Google OAuth state expired. Try connecting again.', 'calio-addon'),
            ]);
            $this->redirect_to_settings();
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $oauthError = isset($_GET['error']) ? sanitize_text_field((string) wp_unslash($_GET['error'])) : '';
        if ($oauthError !== '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => sprintf(
                    /* translators: %s: Google OAuth error code */
                    __('Google returned an OAuth error: %s', 'calio-addon'),
                    $oauthError
                ),
            ]);
            $this->redirect_to_settings();
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $code = isset($_GET['code']) ? sanitize_text_field((string) wp_unslash($_GET['code'])) : '';
        if ($code === '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Google did not return an authorization code.', 'calio-addon'),
            ]);
            $this->redirect_to_settings();
        }

        $settings = Settings::api_settings();
        $tokenResponse = self::request_token([
            'code' => $code,
            'client_id' => trim((string) ($settings['google_calendar_client_id'] ?? '')),
            'client_secret' => trim((string) ($settings['google_calendar_client_secret'] ?? '')),
            'redirect_uri' => self::redirect_uri(),
            'grant_type' => 'authorization_code',
        ]);

        if (is_wp_error($tokenResponse)) {
            Settings::update_api_settings([
                'google_calendar_last_error' => $tokenResponse->get_error_message(),
            ]);
            $this->redirect_to_settings();
        }

        $accessToken = trim((string) ($tokenResponse['access_token'] ?? ''));
        if ($accessToken === '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Google did not return an access token.', 'calio-addon'),
            ]);
            $this->redirect_to_settings();
        }

        $identity = self::extract_identity($tokenResponse, $accessToken);
        $calendars = self::fetch_calendar_list($accessToken);
        $primaryCalendar = self::pick_primary_calendar($calendars);
        $refreshToken = trim((string) ($tokenResponse['refresh_token'] ?? ''));

        Settings::update_api_settings([
            'google_calendar_access_token' => $accessToken,
            'google_calendar_refresh_token' => $refreshToken !== '' ? $refreshToken : (string) ($settings['google_calendar_refresh_token'] ?? ''),
            'google_calendar_scope' => sanitize_text_field((string) ($tokenResponse['scope'] ?? implode(' ', self::scopes()))),
            'google_calendar_token_expires_at' => time() + max(0, (int) ($tokenResponse['expires_in'] ?? HOUR_IN_SECONDS) - self::CLOCK_SKEW),
            'google_calendar_connected_email' => $identity['email'],
            'google_calendar_connected_name' => $identity['name'],
            'google_calendar_connected_calendar' => $primaryCalendar['summary'] ?? '',
            'google_calendar_connected_calendar_id' => $primaryCalendar['id'] ?? '',
            'google_calendar_connected_at' => time(),
            'google_calendar_last_error' => '',
            'google_calendar_calendar_cache' => $calendars,
        ]);

        $this->redirect_to_settings();
    }

    public function handle_disconnect_request(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You are not allowed to disconnect Google Calendar.', 'calio-addon'));
        }

        check_admin_referer(self::NONCE_DISCONNECT);

        $settings = Settings::api_settings();
        $token = trim((string) ($settings['google_calendar_refresh_token'] ?? ''));
        if ($token === '') {
            $token = trim((string) ($settings['google_calendar_access_token'] ?? ''));
        }

        if ($token !== '') {
            wp_safe_remote_post(self::REVOKE_ENDPOINT, [
                'timeout' => 8,
                'body' => [
                    'token' => $token,
                ],
                'headers' => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
            ]);
        }

        Settings::clear_google_calendar_oauth_state();
        $this->redirect_to_settings();
    }

    public function handle_test_request(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You are not allowed to test Google Calendar.', 'calio-addon'));
        }

        check_admin_referer(self::NONCE_TEST);

        $accessToken = self::get_valid_access_token();
        if ($accessToken === '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Google Calendar is not connected or the access token could not be refreshed.', 'calio-addon'),
            ]);
            $this->redirect_to_settings(['calio_google_status' => 'error']);
        }

        $identity = self::extract_identity([], $accessToken);
        $calendars = self::fetch_calendar_list($accessToken);

        if (!$calendars) {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Connected, but Google did not return any readable calendars. Check calendar permissions.', 'calio-addon'),
            ]);
            $this->redirect_to_settings(['calio_google_status' => 'warning']);
        }

        $primaryCalendar = self::pick_primary_calendar($calendars);
        Settings::update_api_settings([
            'google_calendar_connected_email' => $identity['email'],
            'google_calendar_connected_name' => $identity['name'],
            'google_calendar_connected_calendar' => $primaryCalendar['summary'] ?? '',
            'google_calendar_connected_calendar_id' => $primaryCalendar['id'] ?? '',
            'google_calendar_calendar_cache' => $calendars,
            'google_calendar_last_error' => '',
        ]);

        $this->redirect_to_settings(['calio_google_status' => 'tested']);
    }

    public function handle_refresh_request(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You are not allowed to refresh Google Calendar.', 'calio-addon'));
        }

        check_admin_referer(self::NONCE_REFRESH);

        $accessToken = self::get_valid_access_token();
        if ($accessToken === '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Refresh failed because the Google Calendar connection is not active.', 'calio-addon'),
            ]);
            $this->redirect_to_settings(['calio_google_status' => 'error']);
        }

        $calendars = self::fetch_calendar_list($accessToken);
        if (!$calendars) {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Google Calendar refresh returned no calendars. Try reconnecting if this keeps happening.', 'calio-addon'),
            ]);
            $this->redirect_to_settings(['calio_google_status' => 'warning']);
        }

        $primaryCalendar = self::pick_primary_calendar($calendars);
        Settings::update_api_settings([
            'google_calendar_connected_calendar' => $primaryCalendar['summary'] ?? '',
            'google_calendar_connected_calendar_id' => $primaryCalendar['id'] ?? '',
            'google_calendar_calendar_cache' => $calendars,
            'google_calendar_last_error' => '',
        ]);

        $this->redirect_to_settings(['calio_google_status' => 'refreshed']);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private static function refresh_access_token(array $settings): string
    {
        $refreshToken = trim((string) ($settings['google_calendar_refresh_token'] ?? ''));
        $clientId = trim((string) ($settings['google_calendar_client_id'] ?? ''));
        $clientSecret = trim((string) ($settings['google_calendar_client_secret'] ?? ''));

        if ($refreshToken === '' || $clientId === '' || $clientSecret === '') {
            return '';
        }

        $tokenResponse = self::request_token([
            'client_id' => $clientId,
            'client_secret' => $clientSecret,
            'refresh_token' => $refreshToken,
            'grant_type' => 'refresh_token',
        ]);

        if (is_wp_error($tokenResponse)) {
            Settings::update_api_settings([
                'google_calendar_last_error' => $tokenResponse->get_error_message(),
            ]);

            return '';
        }

        $accessToken = trim((string) ($tokenResponse['access_token'] ?? ''));
        if ($accessToken === '') {
            Settings::update_api_settings([
                'google_calendar_last_error' => __('Google refresh response was missing an access token.', 'calio-addon'),
            ]);

            return '';
        }

        Settings::update_api_settings([
            'google_calendar_access_token' => $accessToken,
            'google_calendar_refresh_token' => trim((string) ($tokenResponse['refresh_token'] ?? '')) !== ''
                ? trim((string) ($tokenResponse['refresh_token'] ?? ''))
                : $refreshToken,
            'google_calendar_scope' => sanitize_text_field((string) ($tokenResponse['scope'] ?? ($settings['google_calendar_scope'] ?? ''))),
            'google_calendar_token_expires_at' => time() + max(0, (int) ($tokenResponse['expires_in'] ?? HOUR_IN_SECONDS) - self::CLOCK_SKEW),
            'google_calendar_last_error' => '',
        ]);

        return $accessToken;
    }

    /**
     * @param array<string, string> $body
     * @return array<string, mixed>|\WP_Error
     */
    private static function request_token(array $body)
    {
        $response = wp_safe_remote_post(self::TOKEN_ENDPOINT, [
            'timeout' => 12,
            'body' => $body,
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);

        if (is_wp_error($response)) {
            return new \WP_Error('calio_google_oauth_http', $response->get_error_message());
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $payload = json_decode((string) wp_remote_retrieve_body($response), true);
        if (!is_array($payload)) {
            $payload = [];
        }

        if ($code >= 400) {
            $message = sanitize_text_field((string) ($payload['error_description'] ?? $payload['error'] ?? __('Google token exchange failed.', 'calio-addon')));

            return new \WP_Error('calio_google_oauth_token', $message);
        }

        return $payload;
    }

    /**
     * @return array<int, array{id: string, summary: string, primary: bool, color: string, access_role: string, time_zone: string}>
     */
    private static function fetch_calendar_list(string $accessToken): array
    {
        $response = wp_safe_remote_get(add_query_arg([
            'minAccessRole' => 'reader',
            'showHidden' => 'false',
        ], self::CALENDAR_LIST_ENDPOINT), [
            'timeout' => 12,
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . $accessToken,
            ],
        ]);

        if (is_wp_error($response)) {
            return [];
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $payload = json_decode((string) wp_remote_retrieve_body($response), true);
        $items = is_array($payload['items'] ?? null) ? $payload['items'] : [];

        if ($code >= 400 || !$items) {
            return [];
        }

        $calendars = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }

            $id = sanitize_text_field((string) ($item['id'] ?? ''));
            $summary = sanitize_text_field((string) ($item['summaryOverride'] ?? $item['summary'] ?? ''));
            if ($id === '' || $summary === '') {
                continue;
            }

            $calendars[] = [
                'id' => $id,
                'summary' => $summary,
                'primary' => !empty($item['primary']),
                'color' => sanitize_hex_color((string) ($item['backgroundColor'] ?? '')) ?: '',
                'access_role' => sanitize_key((string) ($item['accessRole'] ?? '')),
                'time_zone' => sanitize_text_field((string) ($item['timeZone'] ?? '')),
            ];
        }

        usort($calendars, static function (array $left, array $right): int {
            if ($left['primary'] === $right['primary']) {
                return strcmp($left['summary'], $right['summary']);
            }

            return $left['primary'] ? -1 : 1;
        });

        return $calendars;
    }

    /**
     * @param array<string, mixed> $tokenResponse
     * @return array{name: string, email: string}
     */
    private static function extract_identity(array $tokenResponse, string $accessToken): array
    {
        $response = wp_safe_remote_get(self::USERINFO_ENDPOINT, [
            'timeout' => 10,
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . $accessToken,
            ],
        ]);

        if (is_wp_error($response)) {
            return ['email' => '', 'name' => ''];
        }

        $userInfo = json_decode((string) wp_remote_retrieve_body($response), true);

        return [
            'email' => sanitize_email((string) ($userInfo['email'] ?? '')),
            'name' => sanitize_text_field((string) ($userInfo['name'] ?? '')),
        ];
    }

    /**
     * @param array<int, array{id: string, summary: string, primary: bool, color: string, access_role: string, time_zone: string}> $calendars
     * @return array{id: string, summary: string, primary: bool, color: string, access_role: string, time_zone: string}|array{}
     */
    private static function pick_primary_calendar(array $calendars): array
    {
        foreach ($calendars as $calendar) {
            if (!empty($calendar['primary'])) {
                return $calendar;
            }
        }

        return $calendars[0] ?? [];
    }

    /**
     * @param array<string, scalar> $args
     */
    private function redirect_to_settings(array $args = []): void
    {
        $url = self::settings_url();
        if ($args) {
            $url = add_query_arg($args, $url);
        }

        wp_safe_redirect($url);
        exit;
    }
}
