<?php
declare(strict_types=1);
// ==================================================
// Nuclear Fancy Pants Edition™ — NUCLEAR+++
// File: /includes/class-tiktok-auth.php
// Description: TikTok OAuth flow, token exchange, encrypted token storage, daily refresh cron - NUCLEAR+++
// Last Edited: 2026-04-15
// ==================================================

namespace AutoMarketerAI;

// No direct access
defined('ABSPATH') || exit;

final class TikTokAuth
{
    // TikTok API endpoints
    private const AUTH_BASE  = 'https://www.tiktok.com/v2/auth/authorize/';
    private const TOKEN_URL  = 'https://open.tiktokapis.com/v2/oauth/token/';
    private const USERINFO_URL   = 'https://open.tiktokapis.com/v2/user/info/';
    private const CREATOR_URL    = 'https://open.tiktokapis.com/v2/post/publish/creator_info/query/';
    private const REVOKE_URL     = 'https://open.tiktokapis.com/v2/oauth/revoke/';

    // Scopes we request
    private const SCOPES = [
        'user.info.basic',
        'video.publish',
    ];

    // wp_options key for the OAuth state nonce
    private const STATE_OPTION = 'amai_tk_oauth_state';

    // WP cron hook name
    public const CRON_HOOK = 'amai_tk_token_refresh';

    // Refresh access token when fewer than this many hours remain
    // TikTok access tokens last 24 hours - refresh aggressively
    private const REFRESH_THRESHOLD_HOURS = 6;

    // Access token lifetime in seconds (24 hours)
    private const ACCESS_TOKEN_TTL = 86400;

    // Refresh token lifetime in seconds (365 days)
    private const REFRESH_TOKEN_TTL = 31536000;

    // OAuth callback query param - must match TikTok app settings exactly
    private const CALLBACK_PARAM = 'amai_tk_callback';

    // wp_options key tracking the attachment ID of the stored avatar
    private const AVATAR_ATTACHMENT_OPTION = 'amai_tk_avatar_attachment_id';

    // --------------------------------------------------
    // Public: build the OAuth authorization URL
    // --------------------------------------------------
    public static function getAuthUrl(): string
    {
        $state = bin2hex(random_bytes(16));
        update_option(self::STATE_OPTION, $state, false);

        $params = [
            'client_key'    => self::getClientKey(),
            'redirect_uri'  => self::getCallbackUrl(),
            'scope'         => implode(',', self::SCOPES),
            'state'         => $state,
            'response_type' => 'code',
        ];

        return self::AUTH_BASE . '?' . http_build_query($params);
    }

    // --------------------------------------------------
    // Public: get the callback URL for TikTok OAuth
    // --------------------------------------------------
    public static function getCallbackUrl(): string
    {
        return home_url('/tiktok/callback');
    }

    // --------------------------------------------------
    // Public: handle the OAuth callback
    // Called from the rewrite rule handler when TikTok
    // redirects back to /tiktok/callback
    // --------------------------------------------------
    public static function handleCallback(): void
    {
        // Verify we have the expected query params
        $code  = sanitize_text_field((string) ($_GET['code'] ?? ''));
        $state = sanitize_text_field((string) ($_GET['state'] ?? ''));
        $error = sanitize_text_field((string) ($_GET['error'] ?? ''));

        if ($error !== '') {
            self::redirectWithError('tiktok_auth_denied');
            return;
        }

        if ($code === '' || $state === '') {
            self::redirectWithError('tiktok_missing_params');
            return;
        }

        // Verify state nonce
        $stored_state = (string) get_option(self::STATE_OPTION, '');
        delete_option(self::STATE_OPTION);

        if (!hash_equals($stored_state, $state)) {
            self::redirectWithError('tiktok_invalid_state');
            return;
        }

        // Exchange code for tokens
        $token_data = self::exchangeCode($code);

        if (isset($token_data['error'])) {
            self::redirectWithError('tiktok_token_exchange_failed');
            return;
        }

        $access_token  = (string) ($token_data['access_token'] ?? '');
        $refresh_token = (string) ($token_data['refresh_token'] ?? '');
        $open_id       = (string) ($token_data['open_id'] ?? '');

        if ($access_token === '' || $open_id === '') {
            self::redirectWithError('tiktok_empty_token');
            return;
        }

        // Fetch user info
        $user_info = self::fetchUserInfo($access_token);

        $username     = (string) ($user_info['username'] ?? '');
        $display_name = (string) ($user_info['display_name'] ?? '');
        $avatar_url   = (string) ($user_info['avatar_url'] ?? '');
        $followers    = (int) ($user_info['follower_count'] ?? 0);

        // Save the connection
        self::saveConnection(
            $open_id,
            $username,
            $display_name,
            $avatar_url,
            $followers,
            $access_token,
            $refresh_token
        );

        // Schedule the daily token refresh cron
        self::scheduleCron();

        // Redirect to the TikTok social page
        wp_safe_redirect(
            admin_url('admin.php?page=automarketer-ai-social&channel=tiktok&connected=1')
        );
        exit;
    }

    // --------------------------------------------------
    // Public: get the stored connection row or null
    // --------------------------------------------------
    public static function getConnection(): ?array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_tiktok_connection';
        $row   = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM `{$table}` WHERE tenant_id = %s AND status = 'active' LIMIT 1",
                'default'
            ),
            ARRAY_A
        );

        return is_array($row) ? $row : null;
    }

    // --------------------------------------------------
    // Public: get the decrypted access token or null
    // --------------------------------------------------
    public static function getAccessToken(): ?string
    {
        $connection = self::getConnection();
        if ($connection === null) {
            return null;
        }

        $encrypted = (string) ($connection['access_token_enc'] ?? '');
        if ($encrypted === '') {
            return null;
        }

        $decrypted = KeyVault::decrypt($encrypted);

        return ($decrypted !== '' && $decrypted !== false) ? $decrypted : null;
    }

    // --------------------------------------------------
    // Public: get the open_id of the connected account
    // --------------------------------------------------
    public static function getOpenId(): ?string
    {
        $connection = self::getConnection();
        if ($connection === null) {
            return null;
        }

        $open_id = (string) ($connection['open_id'] ?? '');

        return $open_id !== '' ? $open_id : null;
    }

    // --------------------------------------------------
    // Public: disconnect - revoke and delete the connection
    // --------------------------------------------------
    public static function disconnect(): bool
    {
        global $wpdb;

        // Attempt to revoke the access token at TikTok
        $token = self::getAccessToken();
        if ($token !== null) {
            wp_remote_post(
                self::REVOKE_URL,
                [
                    'timeout'   => 10,
                    'sslverify' => true,
                    'headers'   => [
                        'Content-Type' => 'application/x-www-form-urlencoded',
                    ],
                    'body' => http_build_query([
                        'client_key'    => self::getClientKey(),
                        'client_secret' => self::getClientSecret(),
                        'token'         => $token,
                    ]),
                ]
            );
        }

        // Delete avatar attachment
        self::deleteAvatarAttachment();

        // Unschedule the refresh cron
        self::unscheduleCron();

        $table = $wpdb->prefix . 'amai_tiktok_connection';

        $result = $wpdb->update(
            $table,
            ['status' => 'revoked'],
            ['tenant_id' => 'default']
        );

        return $result !== false;
    }

    // --------------------------------------------------
    // Public: verify the stored token is still valid
    // Updates last_verified_at on success
    // --------------------------------------------------
    public static function verifyStoredToken(): bool
    {
        global $wpdb;

        $token = self::getAccessToken();
        if ($token === null) {
            return false;
        }

        $user_info = self::fetchUserInfo($token);
        if (isset($user_info['error'])) {
            $wpdb->update(
                $wpdb->prefix . 'amai_tiktok_connection',
                ['status' => 'expired'],
                ['tenant_id' => 'default']
            );
            return false;
        }

        $wpdb->update(
            $wpdb->prefix . 'amai_tiktok_connection',
            ['last_verified_at' => current_time('mysql', true)],
            ['tenant_id' => 'default']
        );

        return true;
    }

    // --------------------------------------------------
    // Public: refresh the stored token if it is within
    // REFRESH_THRESHOLD_HOURS hours of expiry.
    // TikTok access tokens expire every 24 hours so
    // this cron runs daily (or more frequently if needed).
    // Returns true if refresh was not needed or succeeded,
    // false if refresh was needed but failed.
    // --------------------------------------------------
    public static function maybeRefreshToken(): bool
    {
        $connection = self::getConnection();
        if ($connection === null) {
            return true;
        }

        $expires_at = $connection['token_expires_at'] ?? null;
        if (!is_string($expires_at) || $expires_at === '') {
            return self::refreshAccessToken();
        }

        $expires_ts   = strtotime($expires_at);
        $threshold_ts = time() + (self::REFRESH_THRESHOLD_HOURS * HOUR_IN_SECONDS);

        if ($expires_ts === false || $expires_ts > $threshold_ts) {
            return true;
        }

        return self::refreshAccessToken();
    }

    // --------------------------------------------------
    // Public: unconditionally refresh the access token
    // using the stored refresh token.
    // --------------------------------------------------
    public static function refreshAccessToken(): bool
    {
        global $wpdb;

        $connection = self::getConnection();
        if ($connection === null) {
            return false;
        }

        $refresh_enc = (string) ($connection['refresh_token_enc'] ?? '');
        if ($refresh_enc === '') {
            return false;
        }

        $refresh_token = KeyVault::decrypt($refresh_enc);
        if ($refresh_token === '' || $refresh_token === false) {
            return false;
        }

        $result = self::exchangeRefreshToken($refresh_token);

        if (isset($result['error'])) {
            $wpdb->update(
                $wpdb->prefix . 'amai_tiktok_connection',
                ['status' => 'expired'],
                ['tenant_id' => 'default']
            );
            return false;
        }

        $new_access  = (string) ($result['access_token'] ?? '');
        $new_refresh = (string) ($result['refresh_token'] ?? '');
        $new_open_id = (string) ($result['open_id'] ?? '');

        if ($new_access === '') {
            return false;
        }

        $access_enc = KeyVault::encrypt($new_access);
        if ($access_enc === '' || $access_enc === false) {
            return false;
        }

        $update_data = [
            'access_token_enc' => $access_enc,
            'token_expires_at' => gmdate('Y-m-d H:i:s', time() + self::ACCESS_TOKEN_TTL),
        ];

        // TikTok rotates refresh tokens on each use - always store the new one
        if ($new_refresh !== '') {
            $refresh_enc_new = KeyVault::encrypt($new_refresh);
            if ($refresh_enc_new !== '' && $refresh_enc_new !== false) {
                $update_data['refresh_token_enc']  = $refresh_enc_new;
                $update_data['refresh_expires_at'] = gmdate('Y-m-d H:i:s', time() + self::REFRESH_TOKEN_TTL);
            }
        }

        // Update open_id if it changed (unlikely but defensive)
        if ($new_open_id !== '' && $new_open_id !== ($connection['open_id'] ?? '')) {
            $update_data['open_id'] = $new_open_id;
        }

        $wpdb->update(
            $wpdb->prefix . 'amai_tiktok_connection',
            $update_data,
            ['tenant_id' => 'default']
        );

        return true;
    }

    // --------------------------------------------------
    // Public: query the creator info endpoint.
    // Must be called before every publish to get
    // available privacy levels for the connected account.
    // Returns the parsed response or an error array.
    // --------------------------------------------------
    public static function queryCreatorInfo(): array
    {
        $token = self::getAccessToken();
        if ($token === null) {
            return ['error' => 'no_access_token'];
        }

        $response = wp_remote_post(
            self::CREATOR_URL,
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Authorization' => 'Bearer ' . $token,
                    'Content-Type'  => 'application/json; charset=UTF-8',
                ],
                'body' => wp_json_encode(new \stdClass()),
            ]
        );

        return self::parseApiResponse($response);
    }

    // --------------------------------------------------
    // Public: schedule the daily token refresh cron event.
    // TikTok tokens expire every 24 hours so this runs
    // on the twicedaily schedule for safety.
    // --------------------------------------------------
    public static function scheduleCron(): void
    {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time(), 'twicedaily', self::CRON_HOOK);
        }
    }

    // --------------------------------------------------
    // Public: unschedule the token refresh cron event.
    // --------------------------------------------------
    public static function unscheduleCron(): void
    {
        $timestamp = wp_next_scheduled(self::CRON_HOOK);
        if ($timestamp !== false) {
            wp_unschedule_event($timestamp, self::CRON_HOOK);
        }
    }

    // --------------------------------------------------
    // Private: exchange auth code for access + refresh tokens
    // --------------------------------------------------
    private static function exchangeCode(string $code): array
    {
        $response = wp_remote_post(
            self::TOKEN_URL,
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'body' => http_build_query([
                    'client_key'    => self::getClientKey(),
                    'client_secret' => self::getClientSecret(),
                    'code'          => $code,
                    'grant_type'    => 'authorization_code',
                    'redirect_uri'  => self::getCallbackUrl(),
                ]),
            ]
        );

        return self::parseApiResponse($response);
    }

    // --------------------------------------------------
    // Private: exchange a refresh token for a new access token.
    // TikTok rotates refresh tokens on each use - always
    // store the new refresh token returned in the response.
    // --------------------------------------------------
    private static function exchangeRefreshToken(string $refresh_token): array
    {
        $response = wp_remote_post(
            self::TOKEN_URL,
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'body' => http_build_query([
                    'client_key'    => self::getClientKey(),
                    'client_secret' => self::getClientSecret(),
                    'refresh_token' => $refresh_token,
                    'grant_type'    => 'refresh_token',
                ]),
            ]
        );

        return self::parseApiResponse($response);
    }

    // --------------------------------------------------
    // Private: fetch user info from TikTok API
    // --------------------------------------------------
    private static function fetchUserInfo(string $access_token): array
    {
        $response = wp_remote_get(
            self::USERINFO_URL . '?' . http_build_query([
                'fields' => 'open_id,union_id,avatar_url,display_name,username,follower_count',
            ]),
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Authorization' => 'Bearer ' . $access_token,
                ],
            ]
        );

        $parsed = self::parseApiResponse($response);

        if (isset($parsed['error'])) {
            return $parsed;
        }

        // TikTok wraps user data in data.user
        $user = $parsed['data']['user'] ?? [];

        if (!is_array($user) || empty($user)) {
            return ['error' => 'empty_user_data'];
        }

        return $user;
    }

    // --------------------------------------------------
    // Private: save the TikTok connection to the database
    // Uses raw SQL via $wpdb->prepare() + $wpdb->query()
    // because avatar URLs contain % characters.
    // --------------------------------------------------
    private static function saveConnection(
        string $open_id,
        string $username,
        string $display_name,
        string $avatar_url,
        int    $followers,
        string $access_token,
        string $refresh_token
    ): void {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_tiktok_connection';

        // Encrypt tokens before storage
        $access_enc  = KeyVault::encrypt($access_token);
        $refresh_enc = ($refresh_token !== '') ? KeyVault::encrypt($refresh_token) : '';

        // Download avatar locally so the URL never expires
        $local_avatar = self::downloadAvatar($avatar_url);
        $stored_avatar = ($local_avatar !== null) ? $local_avatar : null;

        // Token expiry timestamps
        $token_expires_at   = gmdate('Y-m-d H:i:s', time() + self::ACCESS_TOKEN_TTL);
        $refresh_expires_at = gmdate('Y-m-d H:i:s', time() + self::REFRESH_TOKEN_TTL);
        $now                = current_time('mysql', true);

        // Check if a row already exists for this tenant
        $exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM `{$table}` WHERE tenant_id = %s",
                'default'
            )
        );

        if ((int) $exists > 0) {
            $wpdb->query(
                $wpdb->prepare(
                    "UPDATE `{$table}` SET
                        open_id = %s, username = %s, display_name = %s,
                        avatar_url = %s, followers = %d,
                        access_token_enc = %s, refresh_token_enc = %s,
                        token_expires_at = %s, refresh_expires_at = %s,
                        connected_at = %s, last_verified_at = %s,
                        status = 'active'
                    WHERE tenant_id = %s",
                    $open_id,
                    $username,
                    $display_name,
                    $stored_avatar,
                    $followers,
                    $access_enc,
                    $refresh_enc,
                    $token_expires_at,
                    $refresh_expires_at,
                    $now,
                    $now,
                    'default'
                )
            );
        } else {
            $wpdb->query(
                $wpdb->prepare(
                    "INSERT INTO `{$table}` (
                        tenant_id, open_id, username, display_name,
                        avatar_url, followers,
                        access_token_enc, refresh_token_enc,
                        token_expires_at, refresh_expires_at,
                        connected_at, last_verified_at, status
                    ) VALUES (%s, %s, %s, %s, %s, %d, %s, %s, %s, %s, %s, %s, 'active')",
                    'default',
                    $open_id,
                    $username,
                    $display_name,
                    $stored_avatar,
                    $followers,
                    $access_enc,
                    $refresh_enc,
                    $token_expires_at,
                    $refresh_expires_at,
                    $now,
                    $now
                )
            );
        }
    }

    // --------------------------------------------------
    // Private: download the avatar to WP media library.
    // Returns the local URL or null on failure.
    // Same pattern as FacebookAuth::downloadPagePicture().
    // --------------------------------------------------
    private static function downloadAvatar(string $url): ?string
    {
        if ($url === '') {
            return null;
        }

        // Delete any previously stored avatar attachment
        self::deleteAvatarAttachment();

        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $tmp = download_url($url);
        if (is_wp_error($tmp)) {
            return null;
        }

        $file_array = [
            'name'     => 'tiktok-avatar-' . time() . '.jpg',
            'tmp_name' => $tmp,
        ];

        $attachment_id = media_handle_sideload($file_array, 0, 'TikTok avatar');

        if (is_wp_error($attachment_id)) {
            if (is_file($tmp)) {
                wp_delete_file($tmp);
            }
            return null;
        }

        update_option(self::AVATAR_ATTACHMENT_OPTION, $attachment_id, false);

        return (string) wp_get_attachment_url($attachment_id);
    }

    // --------------------------------------------------
    // Private: delete the stored avatar attachment from
    // the media library. Safe to call when no attachment
    // exists - no-ops cleanly.
    // --------------------------------------------------
    private static function deleteAvatarAttachment(): void
    {
        $attachment_id = (int) get_option(self::AVATAR_ATTACHMENT_OPTION, 0);
        if ($attachment_id <= 0) {
            return;
        }

        wp_delete_attachment($attachment_id, true);
        delete_option(self::AVATAR_ATTACHMENT_OPTION);
    }

    // --------------------------------------------------
    // Private: parse a TikTok API response
    // --------------------------------------------------
    private static function parseApiResponse($response): array
    {
        if (is_wp_error($response)) {
            return ['error' => $response->get_error_message()];
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);

        if (!is_array($data)) {
            return ['error' => 'invalid_json_response'];
        }

        // TikTok error format: {"error": "...", "error_description": "..."}
        // or {"data": {"error_code": N, ...}}
        if (isset($data['error']) && $data['error'] !== '') {
            return ['error' => (string) $data['error']];
        }

        if (isset($data['data']['error_code']) && (int) $data['data']['error_code'] !== 0) {
            return ['error' => 'tiktok_api_error_' . (string) $data['data']['error_code']];
        }

        if ($code < 200 || $code >= 300) {
            return ['error' => 'http_' . $code];
        }

        return $data;
    }

    // --------------------------------------------------
    // Private: redirect back to the social page with an error
    // --------------------------------------------------
    private static function redirectWithError(string $error_code): void
    {
        set_transient('amai_tiktok_auth_error', $error_code, 60);
        wp_safe_redirect(
            admin_url('admin.php?page=automarketer-ai-social&channel=tiktok&auth_error=1')
        );
        exit;
    }

    // --------------------------------------------------
    // Private: get the TikTok Client Key from wp-config.php
    // --------------------------------------------------
    private static function getClientKey(): string
    {
        return defined('AMAI_TK_CLIENT_KEY') ? (string) AMAI_TK_CLIENT_KEY : '';
    }

    // --------------------------------------------------
    // Private: get the TikTok Client Secret from wp-config.php
    // --------------------------------------------------
    private static function getClientSecret(): string
    {
        return defined('AMAI_TK_CLIENT_SECRET') ? (string) AMAI_TK_CLIENT_SECRET : '';
    }
}