<?php
declare(strict_types=1);
// ==================================================
// Nuclear Fancy Pants Edition™ — NUCLEAR+++
// File: /includes/class-facebook-auth.php
// Description: Facebook OAuth flow, token exchange, page picker, encrypted token storage - NUCLEAR+++
// Last Edited: 2026-04-09
// ==================================================

namespace AutoMarketerAI;

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

final class FacebookAuth
{
    // Graph API version — pin this, never use "latest"
    private const GRAPH_VERSION = 'v19.0';
    private const GRAPH_BASE    = 'https://graph.facebook.com/v19.0';

    // Permissions we request — minimum viable set
    private const SCOPES = [
        'pages_show_list',
        'pages_read_engagement',
        'pages_manage_posts',
        'pages_read_user_content',
        'business_management',
    ];
    private const SCOPES_INSTAGRAM = [
        'instagram_basic',
        'instagram_content_publish',
    ];

    // Encryption cipher for stored tokens
    private const CIPHER = 'aes-256-gcm';

    // OAuth callback URL — must match Meta app settings exactly
    private const CALLBACK_PARAM = 'amai_fb_callback';

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

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

    // Refresh when token has fewer than this many days remaining
    private const REFRESH_THRESHOLD_DAYS = 10;

    // Long-lived tokens last approximately this many days
    private const TOKEN_LIFETIME_DAYS = 60;

    // wp_options key tracking the attachment ID of the stored page picture
    private const PICTURE_ATTACHMENT_OPTION = 'amai_fb_page_picture_attachment_id';

    // --------------------------------------------------
    // Public: build the OAuth authorization URL
    // --------------------------------------------------
    public static function getAuthUrl(bool $persist_state = true): string
    {
        if (!defined('AMAI_FB_APP_ID') || (string) AMAI_FB_APP_ID === '') {
            return '';
        }
        $state = bin2hex(random_bytes(16));
        if ($persist_state) {
            update_option(self::STATE_OPTION, $state, false);
        } else {
            $state = (string) get_option(self::STATE_OPTION, $state);
        }

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

        return 'https://www.facebook.com/' . self::GRAPH_VERSION . '/dialog/oauth?' . http_build_query($params);
    }

    // --------------------------------------------------
    // Public: handle the OAuth callback
    // Returns array with 'pages' key on success,
    // or 'error' key on failure
    // --------------------------------------------------
    public static function handleCallback(array $get): array
    {
        // User cancelled
        if (isset($get['error'])) {
            return [
                'error' => sanitize_text_field((string) ($get['error_description'] ?? 'Authorization cancelled')),
            ];
        }

        // Validate state to prevent CSRF
        $state          = sanitize_text_field((string) ($get['state'] ?? ''));
        $expected_state = (string) get_option(self::STATE_OPTION, '');

        if ($state === '' || !hash_equals($expected_state, $state)) {
            return ['error' => 'invalid_state'];
        }

        delete_option(self::STATE_OPTION);

        $code = sanitize_text_field((string) ($get['code'] ?? ''));
        if ($code === '') {
            return ['error' => 'missing_code'];
        }

        // Exchange code for short-lived user access token
        $token_result = self::exchangeCode($code);
        if (isset($token_result['error'])) {
            return $token_result;
        }

        $short_token = (string) $token_result['access_token'];

        // Exchange for long-lived user token
        $long_result = self::getLongLivedToken($short_token);
        if (isset($long_result['error'])) {
            return $long_result;
        }

        $long_token = (string) $long_result['access_token'];

        // Fetch the user's FB user ID
        $me = self::graphGet('/me', ['fields' => 'id,name'], $long_token);
        if (isset($me['error'])) {
            return ['error' => 'failed_to_fetch_user'];
        }

        $fb_user_id = sanitize_text_field((string) ($me['id'] ?? ''));

        // Fetch Pages this user manages
        $pages_result = self::fetchManagedPages($long_token);
        if (isset($pages_result['error'])) {
            return $pages_result;
        }

        return [
            'pages'      => $pages_result['pages'],
            'user_token' => $long_token,
            'fb_user_id' => $fb_user_id,
        ];
    }

    // --------------------------------------------------
    // Public: save the selected page and its token.
    // Fetches and stores the Instagram Business Account ID
    // linked to this page via Graph API.
    // Downloads the page picture to WP media library
    // so the URL never expires.
    // --------------------------------------------------
    public static function savePage(
        string $page_id,
        string $page_name,
        string $page_token,
        string $fb_user_id,
        ?string $page_category,
        ?string $page_picture_url,
        ?int $page_followers
    ): bool {
        global $wpdb;

        $table     = $wpdb->prefix . 'amai_facebook_connection';
        $encrypted = self::encryptToken($page_token);

        if ($encrypted === null) {
            return false;
        }

        // Delete any previously stored page picture attachment before saving new one
        self::deletePictureAttachment();

        // Download the page picture locally so the URL never expires
        $local_picture_url = null;
        if ($page_picture_url !== null && $page_picture_url !== '') {
            $local_picture_url = self::downloadPagePicture($page_picture_url, $page_name);
        }

        // Fetch Instagram Business Account ID linked to this Facebook Page
        $instagram_account_id = self::fetchInstagramAccountId($page_id, $page_token);

        // Upsert — replace existing row for this tenant
        $existing = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM `{$table}` WHERE tenant_id = %s LIMIT 1",
                'default'
            )
        );

        // Token expires approximately 60 days from now
        $expires_at = gmdate('Y-m-d H:i:s', time() + (self::TOKEN_LIFETIME_DAYS * DAY_IN_SECONDS));

        if ($existing) {
            $sql = $wpdb->prepare(
                "UPDATE `{$table}` SET
                    page_id = %s, page_name = %s, page_category = %s,
                    page_picture_url = %s, page_followers = %d,
                    access_token_enc = %s, token_expires_at = %s,
                    instagram_account_id = %s,
                    user_fb_id = %s, last_verified_at = %s, status = %s
                WHERE tenant_id = %s",
                sanitize_text_field($page_id),
                sanitize_text_field($page_name),
                $page_category !== null ? sanitize_text_field($page_category) : null,
                $local_picture_url,
                (int) $page_followers,
                $encrypted,
                $expires_at,
                $instagram_account_id,
                sanitize_text_field($fb_user_id),
                current_time('mysql', true),
                'active',
                'default'
            );
        } else {
            $sql = $wpdb->prepare(
                "INSERT INTO `{$table}`
                    (tenant_id, page_id, page_name, page_category, page_picture_url,
                     page_followers, access_token_enc, token_expires_at,
                     instagram_account_id, user_fb_id, last_verified_at, status)
                VALUES (%s, %s, %s, %s, %s, %d, %s, %s, %s, %s, %s, %s)",
                'default',
                sanitize_text_field($page_id),
                sanitize_text_field($page_name),
                $page_category !== null ? sanitize_text_field($page_category) : null,
                $local_picture_url,
                (int) $page_followers,
                $encrypted,
                $expires_at,
                $instagram_account_id,
                sanitize_text_field($fb_user_id),
                current_time('mysql', true),
                'active'
            );
        }

        $result = $wpdb->query($sql);
        return $result !== false;
    }

    // --------------------------------------------------
    // Public: retrieve and decrypt the stored page token
    // --------------------------------------------------
    public static function getPageToken(): ?string
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_facebook_connection';
        $enc   = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT access_token_enc FROM `{$table}` WHERE tenant_id = %s AND status = 'active' LIMIT 1",
                'default'
            )
        );

        if (!is_string($enc) || $enc === '') {
            return null;
        }

        return self::decryptToken($enc);
    }

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

        $table = $wpdb->prefix . 'amai_facebook_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 stored Instagram Business Account ID
    // linked to the active Facebook Page connection.
    // Returns null if not connected or not linked.
    // --------------------------------------------------
    public static function getInstagramAccountId(): ?string
    {
        global $wpdb;

        $table = $wpdb->prefix . 'amai_facebook_connection';
        $id    = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT instagram_account_id FROM `{$table}` WHERE tenant_id = %s AND status = 'active' LIMIT 1",
                'default'
            )
        );

        return (is_string($id) && $id !== '') ? $id : null;
    }

    // --------------------------------------------------
    // Public: revoke the stored connection and clean up
    // the locally stored page picture attachment.
    // --------------------------------------------------
    public static function disconnect(): bool
    {
        global $wpdb;

        // Delete the locally stored page picture from the media library
        self::deletePictureAttachment();

        $table  = $wpdb->prefix . 'amai_facebook_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::getPageToken();
        if ($token === null) {
            return false;
        }

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

        $result = self::graphGet('/me', ['fields' => 'id'], $token);
        if (isset($result['error'])) {
            $wpdb->update(
                $wpdb->prefix . 'amai_facebook_connection',
                ['status' => 'expired'],
                ['tenant_id' => 'default']
            );
            return false;
        }

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

        return true;
    }

    // --------------------------------------------------
    // Public: refresh the stored page token if it is
    // within REFRESH_THRESHOLD_DAYS days of expiry.
    // Safe to call daily — no-ops when not 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 (!is_array($connection)) {
            return true;
        }

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

        $expires_ts   = strtotime($expires_at);
        $threshold_ts = time() + (self::REFRESH_THRESHOLD_DAYS * DAY_IN_SECONDS);

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

        return self::refreshPageToken();
    }

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

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

        $result = self::getLongLivedToken($token);
        if (isset($result['error'])) {
            return false;
        }

        $new_token = (string) ($result['access_token'] ?? '');
        if ($new_token === '') {
            return false;
        }

        $encrypted = self::encryptToken($new_token);
        if ($encrypted === null) {
            return false;
        }

        $expires_at = gmdate('Y-m-d H:i:s', time() + (self::TOKEN_LIFETIME_DAYS * DAY_IN_SECONDS));

        $updated = $wpdb->update(
            $wpdb->prefix . 'amai_facebook_connection',
            [
                'access_token_enc' => $encrypted,
                'token_expires_at' => $expires_at,
                'last_verified_at' => current_time('mysql', true),
                'status'           => 'active',
            ],
            ['tenant_id' => 'default']
        );

        return $updated !== false;
    }

    // --------------------------------------------------
    // Public: schedule the daily token refresh cron event.
    // --------------------------------------------------
    public static function scheduleDailyRefresh(): void
    {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time(), 'daily', self::CRON_HOOK);
        }
    }

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

    // --------------------------------------------------
    // Public: sign a base64-encoded pending payload.
    // Used to pass pending page data through the picker
    // form without relying on DB persistence.
    // --------------------------------------------------
    public static function signPending(string $payload): string
    {
        return hash_hmac('sha256', $payload, self::getAppSecret());
    }

    // --------------------------------------------------
    // Public: verify a signed pending payload.
    // Returns decoded array on success, null on failure.
    // --------------------------------------------------
    public static function verifyPending(string $payload, string $sig): ?array
    {
        if ($payload === '' || $sig === '') {
            return null;
        }

        $expected = self::signPending($payload);
        if (!hash_equals($expected, $sig)) {
            return null;
        }

        $decoded = base64_decode($payload, true);
        if ($decoded === false) {
            return null;
        }

        $data = json_decode($decoded, true);
        if (!is_array($data)) {
            return null;
        }

        return $data;
    }

    // --------------------------------------------------
    // Public: build the OAuth callback URL
    // --------------------------------------------------
    public static function getCallbackUrl(): string
    {
        return admin_url('admin.php?page=automarketer-ai-social&channel=facebook&' . self::CALLBACK_PARAM . '=1');
    }

    // --------------------------------------------------
    // Private: fetch the Instagram Business Account ID
    // linked to a Facebook Page via Graph API.
    // Returns null if not linked or on any error.
    // --------------------------------------------------
    private static function fetchInstagramAccountId(string $page_id, string $page_token): ?string
    {
        $result = self::graphGet('/' . $page_id, ['fields' => 'instagram_business_account'], $page_token);
        if (isset($result['error'])) {
            return null;
        }

        $id = $result['instagram_business_account']['id'] ?? null;
        return (is_string($id) && $id !== '') ? sanitize_text_field($id) : null;
    }

    // --------------------------------------------------
    // Private: download the Facebook page picture to the
    // WP media library and return the local URL.
    // Returns null if the download fails — callers should
    // store null rather than the expiring Meta URL.
    // --------------------------------------------------
    private static function downloadPagePicture(string $remote_url, string $page_name): ?string
    {
        if (!function_exists('wp_upload_bits') || !function_exists('wp_insert_attachment')) {
            return null;
        }

        $response = wp_remote_get($remote_url, ['timeout' => 15, 'sslverify' => true]);
        if (is_wp_error($response)) {
            return null;
        }

        $body = wp_remote_retrieve_body($response);
        if ($body === '') {
            return null;
        }

        $content_type = wp_remote_retrieve_header($response, 'content-type');
        $extension    = 'jpg';
        if (is_string($content_type)) {
            if (str_contains($content_type, 'png'))  { $extension = 'png'; }
            if (str_contains($content_type, 'gif'))  { $extension = 'gif'; }
            if (str_contains($content_type, 'webp')) { $extension = 'webp'; }
        }

        $filename = 'amai-fb-page-' . sanitize_title($page_name) . '.' . $extension;
        $upload   = wp_upload_bits($filename, null, $body);

        if (!empty($upload['error']) || empty($upload['file'])) {
            return null;
        }

        // Register in media library so wp_delete_attachment() works cleanly
        $attachment_id = wp_insert_attachment(
            [
                'post_title'     => sanitize_text_field($page_name) . ' - Facebook page picture',
                'post_content'   => '',
                'post_status'    => 'inherit',
                'post_mime_type' => 'image/' . $extension,
            ],
            $upload['file']
        );

        if (is_wp_error($attachment_id) || $attachment_id === 0) {
            // File saved but not registered — still usable, just won't clean up via attachment ID
            return esc_url_raw($upload['url']);
        }

        // Track the attachment ID so disconnect() can delete it
        update_option(self::PICTURE_ATTACHMENT_OPTION, $attachment_id, false);

        return esc_url_raw($upload['url']);
    }

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

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

    // --------------------------------------------------
    // Private: exchange auth code for short-lived token
    // --------------------------------------------------
    private static function exchangeCode(string $code): array
    {
        $response = wp_remote_get(
            self::GRAPH_BASE . '/oauth/access_token?' . http_build_query([
                'client_id'     => self::getAppId(),
                'client_secret' => self::getAppSecret(),
                'redirect_uri'  => self::getCallbackUrl(),
                'code'          => $code,
            ]),
            ['timeout' => 15, 'sslverify' => true]
        );

        return self::parseGraphResponse($response);
    }

    // --------------------------------------------------
    // Private: exchange short-lived for long-lived token
    // Long-lived tokens last ~60 days
    // --------------------------------------------------
    private static function getLongLivedToken(string $short_token): array
    {
        $response = wp_remote_get(
            self::GRAPH_BASE . '/oauth/access_token?' . http_build_query([
                'grant_type'        => 'fb_exchange_token',
                'client_id'         => self::getAppId(),
                'client_secret'     => self::getAppSecret(),
                'fb_exchange_token' => $short_token,
            ]),
            ['timeout' => 15, 'sslverify' => true]
        );

        return self::parseGraphResponse($response);
    }

    // --------------------------------------------------
    // Private: fetch all Pages the user manages
    // Filters out personal profiles — only Business Pages
    // --------------------------------------------------
    private static function fetchManagedPages(string $user_token): array
    {
        $result = self::graphGet('/me/accounts', [
            'fields' => 'id,name,category,picture,followers_count,access_token,tasks',
        ], $user_token);

        if (isset($result['error'])) {
            return ['error' => 'failed_to_fetch_pages'];
        }

        $raw_pages = $result['data'] ?? [];

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

        $pages = [];
        foreach ($raw_pages as $page) {
            if (!is_array($page)) {
                continue;
            }

            $tasks = (array) ($page['tasks'] ?? []);
            if (!in_array('MANAGE', $tasks, true) && !in_array('CREATE_CONTENT', $tasks, true)) {
                continue;
            }

            $picture_url = null;
            if (isset($page['picture']['data']['url'])) {
                $picture_url = esc_url_raw((string) $page['picture']['data']['url']);
            }

            $pages[] = [
                'id'           => sanitize_text_field((string) ($page['id'] ?? '')),
                'name'         => sanitize_text_field((string) ($page['name'] ?? '')),
                'category'     => sanitize_text_field((string) ($page['category'] ?? '')),
                'picture_url'  => $picture_url,
                'followers'    => (int) ($page['followers_count'] ?? 0),
                'access_token' => (string) ($page['access_token'] ?? ''),
            ];
        }

        if (empty($pages)) {
            return ['error' => 'no_business_pages_found'];
        }

        return ['pages' => $pages];
    }

    // --------------------------------------------------
    // Private: make a GET request to the Graph API
    // --------------------------------------------------
    private static function graphGet(string $path, array $params, string $token): array
    {
        $params['access_token'] = $token;

        $response = wp_remote_get(
            self::GRAPH_BASE . $path . '?' . http_build_query($params),
            ['timeout' => 15, 'sslverify' => true]
        );

        return self::parseGraphResponse($response);
    }

    // --------------------------------------------------
    // Private: parse a wp_remote_get/post response
    // --------------------------------------------------
    private static function parseGraphResponse(mixed $response): array
    {
        if (is_wp_error($response)) {
            return ['error' => 'http_error', 'message' => $response->get_error_message()];
        }

        $body = wp_remote_retrieve_body($response);
        if ($body === '') {
            return ['error' => 'empty_response'];
        }

        $decoded = json_decode($body, true);
        if (!is_array($decoded)) {
            return ['error' => 'invalid_json'];
        }

        if (isset($decoded['error'])) {
            return [
                'error'   => 'graph_error',
                'message' => sanitize_text_field((string) ($decoded['error']['message'] ?? 'Unknown Graph API error')),
                'code'    => (int) ($decoded['error']['code'] ?? 0),
            ];
        }

        return $decoded;
    }

    // --------------------------------------------------
    // Private: AES-256-GCM encrypt a token for storage
    // --------------------------------------------------
    private static function encryptToken(string $token): ?string
    {
        $secret = self::getAppSecret();
        if ($secret === '') {
            return null;
        }

        $key  = hash('sha256', $secret, true);
        $iv   = random_bytes(12);
        $tag  = '';

        $ciphertext = openssl_encrypt(
            $token,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',
            16
        );

        if ($ciphertext === false) {
            return null;
        }

        return base64_encode($iv . $tag . $ciphertext);
    }

    // --------------------------------------------------
    // Private: decrypt a stored token
    // --------------------------------------------------
    private static function decryptToken(string $encrypted): ?string
    {
        $secret = self::getAppSecret();
        if ($secret === '') {
            return null;
        }

        $raw = base64_decode($encrypted, true);
        if ($raw === false || strlen($raw) < 28) {
            return null;
        }

        $key        = hash('sha256', $secret, true);
        $iv         = substr($raw, 0, 12);
        $tag        = substr($raw, 12, 16);
        $ciphertext = substr($raw, 28);

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        return $plaintext === false ? null : $plaintext;
    }

    // --------------------------------------------------
    // Private: get the App ID from wp-config constant
    // --------------------------------------------------
    private static function getAppId(): string
    {
        if (!defined('AMAI_FB_APP_ID') || !is_string(AMAI_FB_APP_ID) || AMAI_FB_APP_ID === '') {
            return '';
        }

        return AMAI_FB_APP_ID;
    }

    // --------------------------------------------------
    // Private: get the App Secret from wp-config constant
    // --------------------------------------------------
    private static function getAppSecret(): string
    {
        if (!defined('AMAI_FB_APP_SECRET') || !is_string(AMAI_FB_APP_SECRET) || AMAI_FB_APP_SECRET === '') {
            return '';
        }

        return AMAI_FB_APP_SECRET;
    }
}