<?php
if (!defined('ABSPATH')) {
    exit;
}

/**
 * LinkedIn integration (OAuth2) for personal profile posting.
 *
 * This connection is intentionally limited to personal profiles.
 * Company pages and organization publishing are not supported here.
 */
class Calliope_SP_LinkedIn {

    private $auth_base = 'https://www.linkedin.com/oauth/v2';
    private $api_base = 'https://api.linkedin.com';
    private $api_version = '202511';

    public function get_client_id() {
        return (string) get_option('calliope_sp_li_client_id', '');
    }

    public function get_client_secret() {
        return (string) get_option('calliope_sp_li_client_secret', '');
    }

    public function get_access_token() {
        return (string) get_option('calliope_sp_li_access_token', '');
    }

    public function get_expires_at() {
        return (int) get_option('calliope_sp_li_expires_at', 0);
    }

    public function get_person_id() {
        return (string) get_option('calliope_sp_li_person_id', '');
    }

    public function get_redirect_uri() {
        return rest_url('calliope-social-publisher/v1/linkedin/callback');
    }

    public function has_saved_credentials() {
        return $this->get_client_id() !== '' && $this->get_client_secret() !== '';
    }

    public function sanitize_oauth_scope_mode($scope_mode, $target = null) {
        return 'member';
    }

    public function get_saved_scope_list() {
        $saved_scopes = get_option('calliope_sp_li_last_scope_list', []);

        if (is_string($saved_scopes)) {
            $saved_scopes = preg_split('/\s+/', trim($saved_scopes));
        }

        if (!is_array($saved_scopes)) {
            $saved_scopes = [];
        }

        return array_values(array_unique(array_filter(array_map('sanitize_text_field', $saved_scopes))));
    }

    public function token_has_scopes($required_scopes) {
        $saved_scopes = $this->get_saved_scope_list();

        foreach ((array) $required_scopes as $scope) {
            if (!in_array((string) $scope, $saved_scopes, true)) {
                return false;
            }
        }

        return true;
    }

    public function get_oauth_scope_list_for_target($target = null, $scope_mode = 'member') {
        return ['openid', 'profile', 'email', 'w_member_social'];
    }

    public function get_oauth_scopes_for_target($target = null, $scope_mode = 'member') {
        return implode(' ', $this->get_oauth_scope_list_for_target($target, $scope_mode));
    }

    public function get_oauth_scopes_for_saved_target() {
        return $this->get_oauth_scopes_for_target($this->resolve_publish_target());
    }

    public function is_organization_target() {
        $target = $this->resolve_publish_target();
        return $target['kind'] === 'organization';
    }

    public function get_publish_target_label() {
        $target = $this->resolve_publish_target();

        if ($target['kind'] === 'organization') {
            return __('LinkedIn Page', 'calliope-social-publisher');
        }

        if ($target['kind'] === 'person') {
            return __('LinkedIn profile', 'calliope-social-publisher');
        }

        return '';
    }

    public function get_connection_state() {
        $target = $this->resolve_publish_target();

        if (!$this->has_saved_credentials()) {
            return $this->build_connection_state(
                'not_connected',
                'error',
                __('Not connected', 'calliope-social-publisher'),
                __('Add your LinkedIn Client ID and Client Secret, save, and then click Connect with LinkedIn.', 'calliope-social-publisher'),
                $target
            );
        }

        if ($this->get_access_token() === '') {
            return $this->build_connection_state(
                'credentials_saved',
                'warning',
                __('Credentials saved', 'calliope-social-publisher'),
                __('Credentials are saved. Click Connect with LinkedIn to finish the OAuth connection.', 'calliope-social-publisher'),
                $target
            );
        }

        $token_check = $this->test_connection();
        if (is_wp_error($token_check)) {
            if ($token_check->get_error_code() === 'expired') {
                return $this->build_connection_state(
                    'token_expired',
                    'error',
                    __('Reconnect required', 'calliope-social-publisher'),
                    $token_check->get_error_message(),
                    $target
                );
            }

            return $this->build_connection_state(
                'credentials_saved',
                'warning',
                __('Credentials saved', 'calliope-social-publisher'),
                __('Credentials are saved. Click Connect with LinkedIn to finish the OAuth connection.', 'calliope-social-publisher'),
                $target
            );
        }

        if ($target['kind'] === 'none') {
            return $this->build_connection_state(
                'connected_target_missing',
                'warning',
                __('Connected, but profile target missing', 'calliope-social-publisher'),
                __('LinkedIn connected. Now click Use connected profile so the plugin can detect your personal profile author ID automatically, or paste a full Person URN manually.', 'calliope-social-publisher'),
                $target
            );
        }

        if (in_array($target['kind'], ['organization', 'invalid_company_url'], true)) {
            return $this->build_connection_state(
                'connected_target_invalid',
                'warning',
                __('Connected, but profile target is invalid', 'calliope-social-publisher'),
                __('This LinkedIn connection is only for personal profiles. Company pages and business page URLs are not supported here.', 'calliope-social-publisher'),
                $target
            );
        }

        if ($target['kind'] === 'invalid_profile_url') {
            return $this->build_connection_state(
                'connected_target_invalid',
                'warning',
                __('Connected, but profile target is invalid', 'calliope-social-publisher'),
                __('A public LinkedIn profile URL is not enough for publishing. Click Use connected profile or paste a full Person URN instead.', 'calliope-social-publisher'),
                $target
            );
        }

        if (in_array($target['kind'], ['invalid_member_urn', 'unknown'], true)) {
            return $this->build_connection_state(
                'connected_target_invalid',
                'warning',
                __('Connected, but profile target is invalid', 'calliope-social-publisher'),
                __('Paste a full Person URN like urn:li:person:ABC123, or click Use connected profile so the plugin can fill it for you.', 'calliope-social-publisher'),
                $target
            );
        }

        return $this->build_connection_state(
            'ready_to_publish',
            'success',
            __('Ready to publish', 'calliope-social-publisher'),
            __('LinkedIn personal profile is ready to publish.', 'calliope-social-publisher'),
            $target
        );
    }

    /**
     * Accept a simplified input and normalize it to the URN expected by LinkedIn posts.
     */
    public function resolve_publish_target($raw = '') {
        $raw = trim((string) ($raw !== '' ? $raw : $this->get_person_id()));
        $raw = trim(wp_strip_all_tags($raw));

        if ($raw === '') {
            return [
                'kind' => 'none',
                'normalized' => '',
                'id' => '',
                'raw' => '',
            ];
        }

        if (preg_match('#linkedin\.com/company/([0-9]+)#i', $raw, $matches)) {
            return [
                'kind' => 'organization',
                'normalized' => 'urn:li:organization:' . $matches[1],
                'id' => $matches[1],
                'raw' => $raw,
            ];
        }

        if (preg_match('~linkedin\.com/company/([^/?#]+)~i', $raw)) {
            return [
                'kind' => 'invalid_company_url',
                'normalized' => '',
                'id' => '',
                'raw' => $raw,
            ];
        }

        if (preg_match('#^urn:li:organization:([A-Za-z0-9_-]+)$#i', $raw, $matches)) {
            return [
                'kind' => 'organization',
                'normalized' => 'urn:li:organization:' . $matches[1],
                'id' => $matches[1],
                'raw' => $raw,
            ];
        }

        if (preg_match('#^urn:li:company:([A-Za-z0-9_-]+)$#i', $raw, $matches)) {
            return [
                'kind' => 'organization',
                'normalized' => 'urn:li:organization:' . $matches[1],
                'id' => $matches[1],
                'raw' => $raw,
            ];
        }

        if (ctype_digit($raw)) {
            return [
                'kind' => 'organization',
                'normalized' => 'urn:li:organization:' . $raw,
                'id' => $raw,
                'raw' => $raw,
            ];
        }

        if (preg_match('#^urn:li:person:([A-Za-z0-9_-]+)$#i', $raw, $matches)) {
            return [
                'kind' => 'person',
                'normalized' => 'urn:li:person:' . $matches[1],
                'id' => $matches[1],
                'raw' => $raw,
            ];
        }

        foreach (['urn:li:fs_miniProfile:', 'urn:li:fsd_profile:', 'urn:li:fs_profile:'] as $prefix) {
            if (strpos($raw, $prefix) === 0) {
                $tail = substr($raw, strlen($prefix));

                return [
                    'kind' => 'person',
                    'normalized' => $tail ? ('urn:li:person:' . $tail) : '',
                    'id' => $tail,
                    'raw' => $raw,
                ];
            }
        }

        if (preg_match('#linkedin\.com/in/#i', $raw)) {
            return [
                'kind' => 'invalid_profile_url',
                'normalized' => '',
                'id' => '',
                'raw' => $raw,
            ];
        }

        if (strpos($raw, 'urn:li:member:') === 0) {
            return [
                'kind' => 'invalid_member_urn',
                'normalized' => '',
                'id' => '',
                'raw' => $raw,
            ];
        }

        if (preg_match('/^ACo[A-Za-z0-9_-]+$/', $raw)) {
            return [
                'kind' => 'person',
                'normalized' => 'urn:li:person:' . $raw,
                'id' => $raw,
                'raw' => $raw,
            ];
        }

        return [
            'kind' => 'unknown',
            'normalized' => '',
            'id' => '',
            'raw' => $raw,
        ];
    }

    public function build_posts_author_urn() {
        $target = $this->resolve_publish_target();
        return in_array($target['kind'], ['organization', 'person'], true) ? $target['normalized'] : '';
    }

    public function fetch_profile_person_urn() {
        $token = $this->get_access_token();

        if (!$token) {
            return new WP_Error('cfg', __('Missing LinkedIn Access Token. Connect with LinkedIn first.', 'calliope-social-publisher'));
        }

        $userinfo_response = wp_remote_get($this->api_base . '/v2/userinfo', [
            'timeout' => 20,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
            ],
        ]);

        if (!is_wp_error($userinfo_response)) {
            $userinfo_http = (int) wp_remote_retrieve_response_code($userinfo_response);
            $userinfo_raw = (string) wp_remote_retrieve_body($userinfo_response);
            $userinfo_json = json_decode($userinfo_raw, true);

            if ($userinfo_http >= 200 && $userinfo_http < 300 && is_array($userinfo_json) && !empty($userinfo_json['sub'])) {
                return 'urn:li:person:' . sanitize_text_field((string) $userinfo_json['sub']);
            }

            if ($userinfo_http === 401 || $userinfo_http === 403) {
                $message = is_array($userinfo_json) && !empty($userinfo_json['message']) ? (string) $userinfo_json['message'] : $userinfo_raw;
                $message = trim(wp_strip_all_tags($message));

                if (stripos($message, 'permission') !== false || stripos($message, 'openid') !== false) {
                    return new WP_Error('li_profile_scope', __('Automatic profile detection needs a fresh LinkedIn member token with openid, profile, email, and w_member_social. Click Connect with LinkedIn again to refresh the profile token, then retry Use connected profile.', 'calliope-social-publisher'));
                }
            }
        }

        $response = wp_remote_get($this->api_base . '/v2/me', [
            'timeout' => 20,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'X-Restli-Protocol-Version' => '2.0.0',
            ],
        ]);

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

        $http = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        $json = json_decode($raw, true);

        if ($http >= 200 && $http < 300 && is_array($json) && !empty($json['id'])) {
            return 'urn:li:person:' . sanitize_text_field((string) $json['id']);
        }

        if ($http === 401 || $http === 403) {
            return new WP_Error('li_profile_scope', __('Automatic profile detection needs a fresh LinkedIn member token with openid, profile, email, and w_member_social. Click Connect with LinkedIn again to refresh the profile token, then retry Use connected profile.', 'calliope-social-publisher'));
        }

        return new WP_Error('li_profile', __('Automatic profile detection is not available for this LinkedIn app or token.', 'calliope-social-publisher'));
    }

    public function test_connection() {
        $token = $this->get_access_token();

        if (!$token) {
            return new WP_Error('cfg', __('Missing LinkedIn Access Token. Save your settings and connect with LinkedIn.', 'calliope-social-publisher'));
        }

        $expires_at = $this->get_expires_at();
        if ($expires_at && $expires_at < time()) {
            return new WP_Error('expired', __('The LinkedIn token expired. Please reconnect.', 'calliope-social-publisher'));
        }

        return true;
    }

    public function test_publish_setup() {
        $token_check = $this->test_connection();
        if (is_wp_error($token_check)) {
            return $token_check;
        }

        $target = $this->resolve_publish_target();

        if ($target['kind'] === 'none') {
            return new WP_Error('li_target_missing', __('LinkedIn connected. Now click Use connected profile so the plugin can detect your personal profile author ID automatically, or paste a full Person URN manually.', 'calliope-social-publisher'));
        }

        if (in_array($target['kind'], ['organization', 'invalid_company_url'], true)) {
            return new WP_Error('li_target_invalid', __('This LinkedIn connection is only for personal profiles. Company pages and business page URLs are not supported here.', 'calliope-social-publisher'));
        }

        if ($target['kind'] === 'invalid_profile_url') {
            return new WP_Error('li_target_invalid', __('A public LinkedIn profile URL is not enough for publishing. Click Use connected profile or paste a full Person URN instead.', 'calliope-social-publisher'));
        }

        if (in_array($target['kind'], ['invalid_member_urn', 'unknown'], true)) {
            return new WP_Error('li_target_invalid', __('Paste a full Person URN like urn:li:person:ABC123, or click Use connected profile so the plugin can fill it for you.', 'calliope-social-publisher'));
        }

        return true;
    }

    public function validate_organization_permissions($organization_urn) {
        $token = $this->get_access_token();

        if (!$token) {
            return new WP_Error('cfg', __('Missing LinkedIn Access Token. Save your settings and connect with LinkedIn.', 'calliope-social-publisher'));
        }

        $url = add_query_arg([
            'q' => 'roleAssignee',
            'state' => 'APPROVED',
            'count' => 100,
            'start' => 0,
        ], $this->api_base . '/rest/organizationAcls');

        $response = wp_remote_get($url, [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'X-Restli-Protocol-Version' => '2.0.0',
                'Linkedin-Version' => $this->api_version,
                'Content-Type' => 'application/json',
            ],
        ]);

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

        $http = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        $json = json_decode($raw, true);

        if ($http >= 200 && $http < 300 && is_array($json)) {
            $allowed_roles = ['ADMINISTRATOR', 'DIRECT_SPONSORED_CONTENT_POSTER', 'CONTENT_ADMIN', 'CONTENT_ADMINISTRATOR'];
            $elements = isset($json['elements']) && is_array($json['elements']) ? $json['elements'] : [];

            foreach ($elements as $element) {
                $organization = isset($element['organization']) ? (string) $element['organization'] : '';
                $role = isset($element['role']) ? strtoupper((string) $element['role']) : '';
                $state = isset($element['state']) ? strtoupper((string) $element['state']) : '';

                if ($organization === $organization_urn && $state === 'APPROVED' && in_array($role, $allowed_roles, true)) {
                    return true;
                }
            }

            return new WP_Error('li_org_role', __('LinkedIn connected, but this account is not an approved admin/content admin for that page. Make sure the connected LinkedIn account is an admin of the selected page.', 'calliope-social-publisher'));
        }

        $message = is_array($json) && !empty($json['message']) ? (string) $json['message'] : $raw;
        $message = trim(wp_strip_all_tags($message));

        if ($http === 401 || $http === 403 || stripos($message, 'permission') !== false) {
            return new WP_Error('li_org_scope', __('LinkedIn saved the connection, but page publishing is not ready yet. Publishing to a LinkedIn Page needs Community Management access plus organization permissions (for example w_organization_social and rw_organization_admin), not only Share on LinkedIn.', 'calliope-social-publisher'));
        }

        /* translators: 1: HTTP status code, 2: LinkedIn API error message. */
        return new WP_Error('li_org_check', sprintf(__('LinkedIn page validation failed (HTTP %1$d). %2$s', 'calliope-social-publisher'), $http, $message));
    }

    public function publish($data) {
        $ready = $this->test_publish_setup();
        if (is_wp_error($ready)) {
            return $ready;
        }

        $token = $this->get_access_token();
        $author = $this->build_posts_author_urn();

        if (!$author) {
            return new WP_Error('cfg', __('LinkedIn is not ready for publishing yet. Check the publishing target field and reconnect if needed.', 'calliope-social-publisher'));
        }

        if (strpos($author, 'urn:li:person:') !== 0) {
            return new WP_Error('li_profile_only', __('This LinkedIn connection is only for personal profiles. Company pages and business publishing are not available here.', 'calliope-social-publisher'));
        }

        $text = $this->format_text($data, 2900);
        $link = !empty($data['link']) ? esc_url_raw($data['link']) : '';

        if ($link && mb_strpos($text, $link) === false) {
            $text = trim($text . "\n\n" . $link);
        }

        $image_urn = '';
        if (!empty($data['image_b64'])) {
            $image_urn = $this->upload_image($author, (string) $data['image_b64']);
            if (is_wp_error($image_urn)) {
                return $image_urn;
            }
        }

        $payload = [
            'author' => $author,
            'commentary' => $text,
            'visibility' => 'PUBLIC',
            'distribution' => [
                'feedDistribution' => 'MAIN_FEED',
                'targetEntities' => [],
                'thirdPartyDistributionChannels' => [],
            ],
            'lifecycleState' => 'PUBLISHED',
            'isReshareDisabledByAuthor' => false,
        ];

        if ($image_urn) {
            $payload['content'] = [
                'media' => [
                    'id' => $image_urn,
                    'altText' => !empty($data['title']) ? wp_strip_all_tags((string) $data['title']) : '',
                ],
            ];
        }

        $response = wp_remote_post($this->api_base . '/rest/posts', [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json; charset=utf-8',
                'X-Restli-Protocol-Version' => '2.0.0',
                'Linkedin-Version' => $this->api_version,
            ],
            'body' => wp_json_encode($payload),
        ]);

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

        $http = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        $json = json_decode($raw, true);
        $message = is_array($json) && !empty($json['message']) ? (string) $json['message'] : $raw;
        $message = trim(wp_strip_all_tags($message));

        if ($http < 200 || $http >= 300) {
            if ($this->is_organization_target() && (stripos($message, 'Organization permissions must be used') !== false || stripos($message, 'permission') !== false)) {
                return new WP_Error('li_publish_org_scope', __('LinkedIn page posting is not fully enabled yet. This app still needs page publishing permissions for organizations. In LinkedIn Developers, Share on LinkedIn alone is not enough for a Page.', 'calliope-social-publisher'));
            }

            return new WP_Error('li_publish', 'HTTP ' . $http . ' | ' . $message);
        }

        return true;
    }

    private function upload_image($owner_urn, $image_b64) {
        $token = $this->get_access_token();
        $image = $this->decode_b64_image($image_b64);

        if (is_wp_error($image)) {
            return $image;
        }

        $init = wp_remote_post($this->api_base . '/rest/images?action=initializeUpload', [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json; charset=utf-8',
                'X-Restli-Protocol-Version' => '2.0.0',
                'Linkedin-Version' => $this->api_version,
            ],
            'body' => wp_json_encode([
                'initializeUploadRequest' => [
                    'owner' => $owner_urn,
                ],
            ]),
        ]);

        if (is_wp_error($init)) {
            return $init;
        }

        $http = (int) wp_remote_retrieve_response_code($init);
        $raw = (string) wp_remote_retrieve_body($init);
        $json = json_decode($raw, true);
        $message = is_array($json) && !empty($json['message']) ? (string) $json['message'] : $raw;
        $message = trim(wp_strip_all_tags($message));

        if ($http < 200 || $http >= 300) {
            if ($this->is_organization_target() && stripos($message, 'Organization permissions must be used') !== false) {
                return new WP_Error('li_img_init', __('LinkedIn accepted the connection, but image publishing for this Page still needs organization permissions. In LinkedIn Developers, a Page needs Community Management access and organization scopes, not only Share on LinkedIn.', 'calliope-social-publisher'));
            }

            return new WP_Error('li_img_init', 'LinkedIn initializeUpload HTTP ' . $http . ' | ' . $message);
        }

        $upload_url = isset($json['value']['uploadUrl']) ? (string) $json['value']['uploadUrl'] : '';
        $image_urn = isset($json['value']['image']) ? (string) $json['value']['image'] : '';

        if (!$upload_url || !$image_urn) {
            return new WP_Error('li_img_parse', __('LinkedIn returned an unexpected response while preparing the image upload.', 'calliope-social-publisher'));
        }

        $put = wp_remote_request($upload_url, [
            'method' => 'PUT',
            'timeout' => 60,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => $image['mime'],
            ],
            'body' => $image['bytes'],
        ]);

        if (is_wp_error($put)) {
            return $put;
        }

        $put_http = (int) wp_remote_retrieve_response_code($put);
        if ($put_http < 200 || $put_http >= 300) {
            $put_raw = trim((string) wp_remote_retrieve_body($put));
            return new WP_Error('li_img_put', 'LinkedIn upload PUT HTTP ' . $put_http . ' | ' . $put_raw);
        }

        return $image_urn;
    }

    private function decode_b64_image($image_b64) {
        $b64 = trim((string) $image_b64);
        $mime = 'image/jpeg';

        if (preg_match('#^data:(image/[^;]+);base64,(.+)$#', $b64, $matches)) {
            $mime = strtolower(trim($matches[1]));
            $b64 = $matches[2];
        }

        $bytes = base64_decode($b64, true);
        if ($bytes === false || $bytes === '') {
            return new WP_Error('img', __('LinkedIn: invalid image.', 'calliope-social-publisher'));
        }

        if (!in_array($mime, ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'], true)) {
            $mime = 'image/jpeg';
        }

        if ($mime === 'image/jpg') {
            $mime = 'image/jpeg';
        }

        return [
            'bytes' => $bytes,
            'mime' => $mime,
        ];
    }

    private function format_text($data, $max_len) {
        $parts = [];

        if (!empty($data['title'])) {
            $parts[] = trim((string) $data['title']);
        }

        if (!empty($data['content'])) {
            $parts[] = trim((string) $data['content']);
        }

        if (!empty($data['hashtags'])) {
            $parts[] = trim((string) $data['hashtags']);
        }

        $text = trim(implode("\n\n", array_filter($parts)));

        if (mb_strlen($text) > $max_len) {
            $text = mb_substr($text, 0, $max_len - 3) . '...';
        }

        return $text;
    }

    private function build_connection_state($key, $tone, $label, $message, $target) {
        return [
            'key' => (string) $key,
            'tone' => in_array($tone, ['success', 'warning', 'error'], true) ? $tone : 'error',
            'label' => (string) $label,
            'message' => (string) $message,
            'is_ready' => $key === 'ready_to_publish',
            'target' => is_array($target) ? $target : [],
        ];
    }
}
