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

namespace AutoMarketerAI;

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

final class LinkedInAuth
{
    // LinkedIn API version header — required on all v2 calls
    private const API_VERSION       = '202501';
    private const API_BASE          = 'https://api.linkedin.com/v2';
    private const AUTH_BASE         = 'https://www.linkedin.com/oauth/v2';

    // Scopes — openid + profile for member identity,
    // w_organization_social to post as org,
    // rw_organization_admin to list administered orgs,
    // offline_access for refresh token (auto-renewal)
    // w_organization_social and rw_organization_admin require Marketing Developer Platform approval.
    // offline_access also requires MDP. Restore all three once MDP is approved.
    private const SCOPES = [
        'openid',
        'profile',
        'w_member_social',
        'email',
    ];

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

    // wp_options key for the signed pending payload (org picker)
    private const PENDING_SIG_OPTION = 'amai_li_pending_sig';

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

    // Refresh access token when fewer than this many days remain
    private const REFRESH_THRESHOLD_DAYS = 10;

    // Access token lifetime in seconds (60 days)
    private const ACCESS_TOKEN_TTL = 5184000;

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

    // OAuth callback query param — must match LinkedIn app settings exactly
    private const CALLBACK_PARAM = 'amai_li_callback';

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

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

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

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

    // --------------------------------------------------
    // Public: handle the OAuth callback.
    // Returns array with 'orgs' key on success,
    // or 'error' key on failure.
    // --------------------------------------------------
    public static function handleCallback(array $get): array
    {
        // User cancelled or LinkedIn returned an error
        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 access + refresh tokens
        $token_result = self::exchangeCode($code);
        if (isset($token_result['error'])) {
            return $token_result;
        }

        $access_token  = (string) ($token_result['access_token'] ?? '');
        $refresh_token = (string) ($token_result['refresh_token'] ?? '');
        $expires_in    = (int) ($token_result['expires_in'] ?? self::ACCESS_TOKEN_TTL);
        $refresh_in    = (int) ($token_result['refresh_token_expires_in'] ?? self::REFRESH_TOKEN_TTL);

        if ($access_token === '') {
            return ['error' => 'missing_access_token'];
        }

        // Fetch the authenticated member's identity
        $member = self::fetchMember($access_token);
        if (isset($member['error'])) {
            return $member;
        }

        // Fetch organization pages this member administers
        $orgs_result = self::fetchAdministeredOrgs($access_token);
        if (isset($orgs_result['error'])) {
            return $orgs_result;
        }

        return [
            'orgs'          => $orgs_result['orgs'],
            'access_token'  => $access_token,
            'refresh_token' => $refresh_token,
            'expires_in'    => $expires_in,
            'refresh_in'    => $refresh_in,
            'member_id'     => $member['member_id'],
            'member_name'   => $member['member_name'],
        ];
    }

    // --------------------------------------------------
    // Public: save the selected organization and its tokens.
    // Downloads the org logo to WP media library so the
    // URL never expires. Encrypts both tokens via KeyVault.
    // --------------------------------------------------
    public static function saveOrg(
        string $organization_urn,
        string $organization_id,
        string $organization_name,
        ?string $org_picture_url,
        ?int $org_followers,
        string $access_token,
        string $refresh_token,
        int $expires_in,
        int $refresh_in,
        string $member_id,
        string $member_name
    ): bool {
        global $wpdb;

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

        $enc_access  = KeyVault::encrypt($access_token);
        $enc_refresh = $refresh_token !== '' ? KeyVault::encrypt($refresh_token) : null;

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

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

        // Download the org logo locally so the URL never expires
        $local_picture_url = null;
        if ($org_picture_url !== null && $org_picture_url !== '') {
            $local_picture_url = self::downloadOrgPicture($org_picture_url, $organization_name);
        }

        $now             = time();
        $token_expires   = gmdate('Y-m-d H:i:s', $now + $expires_in);
        $refresh_expires = gmdate('Y-m-d H:i:s', $now + $refresh_in);

        $existing = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM `{$table}` WHERE tenant_id = %s LIMIT 1",
                'default'
            )
        );

        if ($existing) {
            $sql = $wpdb->prepare(
                "UPDATE `{$table}` SET
                    organization_urn   = %s,
                    organization_id    = %s,
                    organization_name  = %s,
                    page_picture_url   = %s,
                    page_followers     = %d,
                    access_token_enc   = %s,
                    refresh_token_enc  = %s,
                    token_expires_at   = %s,
                    refresh_expires_at = %s,
                    member_id          = %s,
                    member_name        = %s,
                    last_verified_at   = %s,
                    status             = %s
                WHERE tenant_id = %s",
                $organization_urn,
                $organization_id,
                sanitize_text_field($organization_name),
                $local_picture_url,
                (int) $org_followers,
                $enc_access,
                $enc_refresh,
                $token_expires,
                $refresh_expires,
                sanitize_text_field($member_id),
                sanitize_text_field($member_name),
                current_time('mysql', true),
                'active',
                'default'
            );
        } else {
            $sql = $wpdb->prepare(
                "INSERT INTO `{$table}`
                    (tenant_id, organization_urn, organization_id, organization_name,
                     page_picture_url, page_followers, access_token_enc, refresh_token_enc,
                     token_expires_at, refresh_expires_at, member_id, member_name,
                     last_verified_at, status)
                VALUES (%s, %s, %s, %s, %s, %d, %s, %s, %s, %s, %s, %s, %s, %s)",
                'default',
                $organization_urn,
                $organization_id,
                sanitize_text_field($organization_name),
                $local_picture_url,
                (int) $org_followers,
                $enc_access,
                $enc_refresh,
                $token_expires,
                $refresh_expires,
                sanitize_text_field($member_id),
                sanitize_text_field($member_name),
                current_time('mysql', true),
                'active'
            );
        }

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

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

        $table = $wpdb->prefix . 'amai_linkedin_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;
        }

        $token = KeyVault::decrypt($enc);
        return $token !== '' ? $token : null;
    }

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

        $table = $wpdb->prefix . 'amai_linkedin_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: revoke the stored connection and clean up
    // the locally stored org picture attachment.
    // --------------------------------------------------
    public static function disconnect(): bool
    {
        global $wpdb;

        self::deletePictureAttachment();

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

        return $result !== false;
    }

    // --------------------------------------------------
    // Public: refresh the access token using the stored
    // refresh token if within REFRESH_THRESHOLD_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::refreshAccessToken();
        }

        $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::refreshAccessToken();
    }

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

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

        $enc_refresh = $connection['refresh_token_enc'] ?? '';
        if (!is_string($enc_refresh) || $enc_refresh === '') {
            return false;
        }

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

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

        $new_access    = (string) ($result['access_token'] ?? '');
        $new_refresh   = (string) ($result['refresh_token'] ?? $refresh_token);
        $expires_in    = (int) ($result['expires_in'] ?? self::ACCESS_TOKEN_TTL);
        $refresh_in    = (int) ($result['refresh_token_expires_in'] ?? self::REFRESH_TOKEN_TTL);

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

        $enc_access      = KeyVault::encrypt($new_access);
        $enc_refresh_new = KeyVault::encrypt($new_refresh);

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

        $now             = time();
        $token_expires   = gmdate('Y-m-d H:i:s', $now + $expires_in);
        $refresh_expires = gmdate('Y-m-d H:i:s', $now + $refresh_in);

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

        $updated = $wpdb->update(
            $table,
            [
                'access_token_enc'   => $enc_access,
                'refresh_token_enc'  => $enc_refresh_new,
                'token_expires_at'   => $token_expires,
                'refresh_expires_at' => $refresh_expires,
                '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 org data through the picker
    // form without relying on DB persistence during
    // OAuth callback (same pattern as FB).
    // --------------------------------------------------
    public static function signPending(string $payload): string
    {
        return hash_hmac('sha256', $payload, self::getClientSecret());
    }

    // --------------------------------------------------
    // 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=linkedin&' . self::CALLBACK_PARAM . '=1');
    }

    // --------------------------------------------------
    // Private: fetch the authenticated member's identity
    // via the OpenID Connect userinfo endpoint.
    // --------------------------------------------------
    private static function fetchMember(string $access_token): array
    {
        $response = wp_remote_get(
            'https://api.linkedin.com/v2/userinfo',
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Authorization'        => 'Bearer ' . $access_token,
                    'LinkedIn-Version'     => self::API_VERSION,
                    'X-Restli-Protocol-Version' => '2.0.0',
                ],
            ]
        );

        $parsed = self::parseApiResponse($response);
        if (isset($parsed['error'])) {
            return $parsed;
        }

        $member_id   = sanitize_text_field((string) ($parsed['sub'] ?? ''));
        $member_name = sanitize_text_field(trim(
            ((string) ($parsed['given_name'] ?? '')) . ' ' . ((string) ($parsed['family_name'] ?? ''))
        ));

        if ($member_id === '') {
            return ['error' => 'missing_member_id'];
        }

        return [
            'member_id'   => $member_id,
            'member_name' => $member_name,
        ];
    }

    // --------------------------------------------------
    // Private: fetch all Organization Pages the member
    // administers. Filters to APPROVED ADMINISTRATOR role
    // only — personal profiles never appear here.
    // --------------------------------------------------
    private static function fetchAdministeredOrgs(string $access_token): array
    {
        $response = wp_remote_get(
            self::API_BASE . '/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&roleAssigneeType=ORGANIZATION&state=APPROVED&count=50',
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Authorization'             => 'Bearer ' . $access_token,
                    'LinkedIn-Version'          => self::API_VERSION,
                    'X-Restli-Protocol-Version' => '2.0.0',
                ],
            ]
        );

        $parsed = self::parseApiResponse($response);
        if (isset($parsed['error'])) {
            return ['error' => 'failed_to_fetch_orgs'];
        }

        $elements = $parsed['elements'] ?? [];
        if (!is_array($elements) || empty($elements)) {
            return ['error' => 'no_orgs_found'];
        }

        $orgs = [];
        foreach ($elements as $acl) {
            if (!is_array($acl)) {
                continue;
            }

            // organization URN is the entity being administered
            $org_urn = (string) ($acl['organization'] ?? '');
            if ($org_urn === '' || !str_starts_with($org_urn, 'urn:li:organization:')) {
                continue;
            }

            $org_id   = str_replace('urn:li:organization:', '', $org_urn);
            $org_data = self::fetchOrgDetails($org_id, $access_token);

            if (isset($org_data['error'])) {
                continue;
            }

            $orgs[] = $org_data;
        }

        if (empty($orgs)) {
            return ['error' => 'no_administered_orgs_found'];
        }

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

    // --------------------------------------------------
    // Private: fetch a single organization's details
    // including name, logo, and follower count.
    // --------------------------------------------------
    private static function fetchOrgDetails(string $org_id, string $access_token): array
    {
        $response = wp_remote_get(
            self::API_BASE . '/organizations/' . rawurlencode($org_id) . '?projection=(id,localizedName,logoV2(original~:playableStreams))',
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Authorization'             => 'Bearer ' . $access_token,
                    'LinkedIn-Version'          => self::API_VERSION,
                    'X-Restli-Protocol-Version' => '2.0.0',
                ],
            ]
        );

        $parsed = self::parseApiResponse($response);
        if (isset($parsed['error'])) {
            return ['error' => 'failed_to_fetch_org_details'];
        }

        $org_name    = sanitize_text_field((string) ($parsed['localizedName'] ?? ''));
        $picture_url = null;

        // Extract logo URL from logoV2 projection
        $streams = $parsed['logoV2']['original~']['elements'] ?? [];
        if (is_array($streams) && !empty($streams)) {
            $picture_url = esc_url_raw((string) ($streams[0]['identifiers'][0]['identifier'] ?? ''));
            if ($picture_url === '') {
                $picture_url = null;
            }
        }

        // Fetch follower count separately
        $followers = self::fetchOrgFollowers($org_id, $access_token);

        return [
            'organization_urn'  => 'urn:li:organization:' . $org_id,
            'organization_id'   => $org_id,
            'organization_name' => $org_name,
            'picture_url'       => $picture_url,
            'followers'         => $followers,
        ];
    }

    // --------------------------------------------------
    // Private: fetch the follower count for an org.
    // Returns 0 on failure — never blocks the flow.
    // --------------------------------------------------
    private static function fetchOrgFollowers(string $org_id, string $access_token): int
    {
        $response = wp_remote_get(
            self::API_BASE . '/networkSizes/urn:li:organization:' . rawurlencode($org_id) . '?edgeType=CompanyFollowedByMember',
            [
                'timeout'   => 10,
                'sslverify' => true,
                'headers'   => [
                    'Authorization'             => 'Bearer ' . $access_token,
                    'LinkedIn-Version'          => self::API_VERSION,
                    'X-Restli-Protocol-Version' => '2.0.0',
                ],
            ]
        );

        $parsed = self::parseApiResponse($response);
        if (isset($parsed['error'])) {
            return 0;
        }

        return (int) ($parsed['firstDegreeSize'] ?? 0);
    }

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

        return self::parseApiResponse($response);
    }

    // --------------------------------------------------
    // Private: exchange a refresh token for a new access token.
    // LinkedIn 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::AUTH_BASE . '/accessToken',
            [
                'timeout'   => 15,
                'sslverify' => true,
                'headers'   => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'body' => http_build_query([
                    'grant_type'    => 'refresh_token',
                    'refresh_token' => $refresh_token,
                    'client_id'     => self::getClientId(),
                    'client_secret' => self::getClientSecret(),
                ]),
            ]
        );

        return self::parseApiResponse($response);
    }

    // --------------------------------------------------
    // Private: download the org logo to the WP media
    // library and return the local URL.
    // Returns null on failure — callers store null rather
    // than an expiring LinkedIn CDN URL.
    // --------------------------------------------------
    private static function downloadOrgPicture(string $remote_url, string $org_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-li-org-' . sanitize_title($org_name) . '.' . $extension;
        $upload   = wp_upload_bits($filename, null, $body);

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

        $attachment_id = wp_insert_attachment(
            [
                'post_title'     => sanitize_text_field($org_name) . ' - LinkedIn organization logo',
                'post_content'   => '',
                'post_status'    => 'inherit',
                'post_mime_type' => 'image/' . $extension,
            ],
            $upload['file']
        );

        if (is_wp_error($attachment_id) || $attachment_id === 0) {
            return esc_url_raw($upload['url']);
        }

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

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

    // --------------------------------------------------
    // Private: delete the stored org picture attachment
    // from the WP media library. No-ops cleanly when
    // no attachment exists.
    // --------------------------------------------------
    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: parse a wp_remote_get/post response
    // --------------------------------------------------
    private static function parseApiResponse(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'];
        }

        // LinkedIn error envelope — serviceErrorCode or message present
        if (isset($decoded['serviceErrorCode']) || (isset($decoded['status']) && (int) $decoded['status'] >= 400)) {
            return [
                'error'   => 'api_error',
                'message' => sanitize_text_field((string) ($decoded['message'] ?? 'Unknown LinkedIn API error')),
                'code'    => (int) ($decoded['serviceErrorCode'] ?? $decoded['status'] ?? 0),
            ];
        }

        return $decoded;
    }

    // --------------------------------------------------
    // Public: returns true when both credentials are
    // defined in wp-config.php. Used by the connect
    // template to avoid wp_die() on unconfigured installs.
    // --------------------------------------------------
    public static function isConfigured(): bool
    {
        return defined('AMAI_LI_CLIENT_ID')
            && is_string(AMAI_LI_CLIENT_ID)
            && AMAI_LI_CLIENT_ID !== ''
            && defined('AMAI_LI_CLIENT_SECRET')
            && is_string(AMAI_LI_CLIENT_SECRET)
            && AMAI_LI_CLIENT_SECRET !== '';
    }

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

        return AMAI_LI_CLIENT_ID;
    }

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

        return AMAI_LI_CLIENT_SECRET;
    }
}