<?php
// phpcs:disable
 namespace flow\social;
if ( ! defined( 'WPINC' ) ) die;
/**
 * FlowFlow.
 *
 * @package   FlowFlow
 * @author    Looks Awesome <email@looks-awesome.com>
 *
 * @link      http://looks-awesome.com
 * @copyright 2014-2016 Looks Awesome
 *
 * LinkedIn feed via Community Management API (REST).
 * Docs: https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api
 */

class FFLinkedIn extends FFHttpRequestFeed {
	private $image;
	private $media;
	private $company;
	private $profileUrl;
	private $profileImageUrl;
	private $orgName;

	/** LinkedIn REST API version header (YYYYMM format). */
	const LI_API_VERSION = '202601';

	public function __construct( $type = null ) {
		if (is_null($type)) $type = 'linkedin';
		parent::__construct( $type );
	}

	/**
	 * Initialise the feed: build the Posts URL and fetch org profile.
	 *
	 * Uses /rest/posts (Posts API is only available under /rest/, NOT /v2/).
	 * Uses /v2/organizations for org profile lookup.
	 *
	 * @param \stdClass $feed
	 */
	public function deferredInit( $feed ) {

		$token = $feed->linkedin_access_token;
		$num   = $this->getCount();
		$this->company = trim($feed->content);

		// If company ID is empty (e.g. feed saved before JS changes), fail early
		if (empty($this->company)) {
			$this->errors[] = array(
				'type'    => $this->getType(),
				'message' => 'LinkedIn Company ID is missing. Please edit the feed and hit "Save" to map the connected company.',
				'url'     => ''
			);
			return;
		}

		// --- REST API headers ---
		$this->setHeader([
			'Authorization: Bearer ' . $token,
			'X-Restli-Protocol-Version: 2.0.0',
			'LinkedIn-Version: ' . self::LI_API_VERSION,
		]);

		// --- Posts endpoint (Community Management API via /rest/) ---
		// The author URN must be URL-encoded in the query string.
		$authorUrn = rawurlencode('urn:li:organization:' . $this->company);
		$this->url = "https://api.linkedin.com/rest/posts?q=author&author={$authorUrn}&count={$num}&sortBy=LAST_MODIFIED";

		// --- Organisation profile (name + logo via /v2/) ---
		$this->accessToken = $token;
		$this->orgName = '';
		$this->profileImageUrl = '';
		$this->profileImageUrn = '';

		$cacheKey = 'ff_li_org_' . $this->company;
		$cachedProfile = function_exists('get_transient') ? get_transient($cacheKey) : false;

		if ($cachedProfile !== false && is_object($cachedProfile)) {
			$profile = $cachedProfile;
		} else {
			$orgUrl = "https://api.linkedin.com/rest/organizations/{$this->company}";
			$data   = $this->getFeedData($orgUrl, 2000, [
				'Authorization: Bearer ' . $token,
				'X-Restli-Protocol-Version: 2.0.0',
				'LinkedIn-Version: ' . self::LI_API_VERSION,
			]);

			if (isset($data['response'])) {
				$profile = json_decode($data['response']);
				if (is_object($profile) && function_exists('set_transient')) {
					set_transient($cacheKey, $profile, DAY_IN_SECONDS);
				}
			} else {
				$profile = null;
			}
		}

		if (is_object($profile)) {
			$this->orgName = isset($profile->localizedName) ? $profile->localizedName : '';

			// logoV2 → cropped image (usually an image URN)
			$logoRef = '';
			if (isset($profile->logoV2->cropped)) {
				$logoRef = $profile->logoV2->cropped;
			} elseif (isset($profile->logoV2->original)) {
				$logoRef = $profile->logoV2->original;
			}
			
			if (!empty($logoRef)) {
				if (strpos($logoRef, 'urn:li:') === 0) {
					$this->profileImageUrn = str_replace('digitalmediaAsset', 'image', $logoRef);
				} else {
					$this->profileImageUrl = $logoRef;
				}
			}
			// Fallback: legacy logoUrl / squareLogoUrl (some API versions)
			if (empty($this->profileImageUrl)) {
				if (!empty($profile->squareLogoUrl)) {
					$this->profileImageUrl = $profile->squareLogoUrl;
				} elseif (!empty($profile->logoUrl)) {
					$this->profileImageUrl = $profile->logoUrl;
				}
			}
		}
	}

	private $profileImageUrn = '';
	private $accessToken = '';

	/** Default cache TTL for resolved image URNs (20 hours). */
	const IMAGE_CACHE_TTL = 72000; // 20 * 3600

	/** Cache TTL for resolved org logo URL (24 hours). */
	const LOGO_CACHE_TTL = 86400; // 24 * 3600

	/** Backoff duration when Images API is rate-limited (2 hours). */
	const IMAGE_BACKOFF_TTL = 7200; // 2 * 3600

	/**
	 * Parse the REST Posts JSON response into an iterable array.
	 */
	protected function items( $request ) {
		$pxml = json_decode($request);
		if (isset($pxml->elements) && is_array($pxml->elements)) {
			$this->resolveImageUrns($pxml->elements);
			return $pxml->elements;
		}
		return array();
	}

	/**
	 * Resolves image URNs to downloadUrls using the LinkedIn Images Batch API.
	 *
	 * Rate-limit optimizations:
	 * - Uses 20-hour transient cache (LinkedIn signed URLs valid 24-72h).
	 * - Respects `downloadUrlExpiresAt` for smarter per-URL TTL.
	 * - Skips API call entirely during a 2-hour backoff after rate-limit errors.
	 * - Resolves the org logo URL separately with a 24-hour cache.
	 */
	private function resolveImageUrns(&$elements) {
		// --- Resolve org logo separately (Change 5) ---
		$this->resolveOrgLogo();

		// Collect URNs from post content only (logo handled above)
		$urns = [];
		foreach ($elements as $item) {
			if (isset($item->content->media->id)) {
				$urns[] = str_replace('digitalmediaAsset', 'image', $item->content->media->id);
			}
			if (isset($item->content->multiImage->images) && is_array($item->content->multiImage->images)) {
				foreach ($item->content->multiImage->images as $img) {
					if (isset($img->id)) {
						$urns[] = str_replace('digitalmediaAsset', 'image', $img->id);
					}
				}
			}
		}
		$urns = array_unique(array_filter($urns));
		if (empty($urns)) return;

		$results = new \stdClass();
		$toResolve = [];

		// Check transient cache first
		foreach ($urns as $urn) {
			$cacheKey = 'ff_li_urn_' . md5($urn);
			$cached = function_exists('get_transient') ? get_transient($cacheKey) : false;
			if ($cached !== false && is_object($cached)) {
				$results->{$urn} = $cached;
			} else {
				$toResolve[] = $urn;
			}
		}

		// If there are unresolved URNs, call the Images Batch API
		if (!empty($toResolve)) {
			// --- Rate-limit backoff check (Change 3) ---
			$backoffActive = function_exists('get_transient') ? get_transient('ff_li_img_backoff') : false;
			if ($backoffActive) {
				// Backoff is active — skip Images API entirely.
				// Posts already in the DB will keep their existing image URLs.
				error_log('[Flow-Flow] LinkedIn Images API backoff active, skipping resolution for ' . count($toResolve) . ' URNs');
			} else {
				$this->fetchImageBatch($toResolve, $results);
			}
		}

		// Map results back to elements
		$this->mapImageResults($elements, $results);
	}

	/**
	 * Resolve the organisation logo URN to a download URL.
	 * Cached separately with a 24-hour TTL since the logo rarely changes (Change 5).
	 */
	private function resolveOrgLogo() {
		if (empty($this->profileImageUrn)) return;

		// Check dedicated logo cache first
		$logoCacheKey = 'ff_li_logo_' . md5($this->profileImageUrn);
		$cachedLogoUrl = function_exists('get_transient') ? get_transient($logoCacheKey) : false;
		if ($cachedLogoUrl !== false && is_string($cachedLogoUrl) && !empty($cachedLogoUrl)) {
			$this->profileImageUrl = $cachedLogoUrl;
			return;
		}

		// Also check the general URN cache as a fallback
		$urnCacheKey = 'ff_li_urn_' . md5($this->profileImageUrn);
		$cached = function_exists('get_transient') ? get_transient($urnCacheKey) : false;
		if ($cached !== false && is_object($cached) && isset($cached->downloadUrl)) {
			$this->profileImageUrl = $cached->downloadUrl;
			if (function_exists('set_transient')) {
				set_transient($logoCacheKey, $cached->downloadUrl, self::LOGO_CACHE_TTL);
			}
			return;
		}

		// Check backoff before making API call
		$backoffActive = function_exists('get_transient') ? get_transient('ff_li_img_backoff') : false;
		if ($backoffActive) return;

		// Resolve the single logo URN via Images API
		$encodedUrn = rawurlencode($this->profileImageUrn);
		$batchUrl = 'https://api.linkedin.com/rest/images?ids=List(' . $encodedUrn . ')';

		try {
			$data = $this->getFeedData($batchUrl, 2000, [
				'Authorization: Bearer ' . $this->accessToken,
				'X-Restli-Protocol-Version: 2.0.0',
				'LinkedIn-Version: ' . self::LI_API_VERSION,
			]);

			if (isset($data['response'])) {
				$imgsResp = json_decode($data['response']);

				// Check for rate-limit error in response body
				if ($this->isImageApiRateLimited($imgsResp, $data)) return;

				if (isset($imgsResp->results->{$this->profileImageUrn}->downloadUrl)) {
					$info = $imgsResp->results->{$this->profileImageUrn};
					$this->profileImageUrl = $info->downloadUrl;

					if (function_exists('set_transient')) {
						$ttl = $this->computeCacheTtl($info);
						// Cache the resolved URL with exact expiry
						set_transient($logoCacheKey, $info->downloadUrl, $ttl);
						// Also populate the general URN cache
						set_transient($urnCacheKey, $info, $ttl);
					}
				}
			} elseif (isset($data['errors']) && !empty($data['errors'])) {
				$this->checkBatchErrorForRateLimit($data['errors']);
			}
		} catch (\Exception $e) {
			$msg = $e->getMessage();
			if ($this->isRateLimitError($msg)) {
				$this->activateImageBackoff('Exception: ' . $msg);
			} else {
				error_log('[Flow-Flow] LinkedIn resolveOrgLogo exception: ' . $msg);
			}
		}
	}

	/**
	 * Fetch image data from the LinkedIn Images Batch API for the given URNs.
	 * Sets rate-limit backoff if the API returns a throttle error.
	 */
	private function fetchImageBatch(array $toResolve, \stdClass $results) {
		$encodedUrns = array_map('rawurlencode', $toResolve);
		$batchUrl = 'https://api.linkedin.com/rest/images?ids=List(' . implode(',', $encodedUrns) . ')';

		try {
			$data = $this->getFeedData($batchUrl, 2000, [
				'Authorization: Bearer ' . $this->accessToken,
				'X-Restli-Protocol-Version: 2.0.0',
				'LinkedIn-Version: ' . self::LI_API_VERSION,
			]);

			if (isset($data['response'])) {
				$imgsResp = json_decode($data['response']);

				// Check for rate-limit error in response body (Change 3)
				if ($this->isImageApiRateLimited($imgsResp, $data)) return;

				if (isset($imgsResp->results)) {
					foreach ($imgsResp->results as $urn => $info) {
						$results->{$urn} = $info;
						// Cache with smart TTL (Change 1 & 2)
						if (function_exists('set_transient')) {
							$ttl = $this->computeCacheTtl($info);
							set_transient('ff_li_urn_' . md5($urn), $info, $ttl);
						}
					}
				}
			} elseif (isset($data['errors']) && !empty($data['errors'])) {
				$this->checkBatchErrorForRateLimit($data['errors']);
			}
		} catch (\Exception $e) {
			$msg = $e->getMessage();
			if ($this->isRateLimitError($msg)) {
				$this->activateImageBackoff('Exception: ' . $msg);
			} else {
				error_log('[Flow-Flow] LinkedIn fetchImageBatch exception: ' . $msg);
			}
		}
	}

	/**
	 * Compute cache TTL for a resolved image using downloadUrlExpiresAt (Change 2).
	 * Falls back to the default 20-hour TTL (Change 1).
	 *
	 * @param object $info Image info object from the Images API
	 * @return int TTL in seconds
	 */
	private function computeCacheTtl($info) {
		if (isset($info->downloadUrlExpiresAt) && is_numeric($info->downloadUrlExpiresAt)) {
			// downloadUrlExpiresAt is in milliseconds
			$expiresAtSec = (int) floor($info->downloadUrlExpiresAt / 1000);
			$remaining = $expiresAtSec - time();
			
			// If it expires in the future, use the exact remaining time
			if ($remaining > 0) {
				return $remaining;
			}
		}
		// Default: 20 hours
		return self::IMAGE_CACHE_TTL;
	}

	/**
	 * Check if the Images API response indicates a rate-limit error.
	 * If so, activate a 2-hour backoff transient (Change 3).
	 *
	 * @param object|null $imgsResp Decoded JSON response
	 * @param array       $data     Raw response data including errors
	 * @return bool True if rate-limited
	 */
	private function isImageApiRateLimited($imgsResp, $data) {
		// Check response body for throttle message
		if (is_object($imgsResp) && isset($imgsResp->message)) {
			if ($this->isRateLimitError($imgsResp->message)) {
				$this->activateImageBackoff('Response: ' . $imgsResp->message);
				return true;
			}
		}
		// Check HTTP errors
		if (isset($data['errors']) && !empty($data['errors'])) {
			return $this->checkBatchErrorForRateLimit($data['errors']);
		}
		return false;
	}

	/**
	 * Check error array for rate-limit indicators and activate backoff if found.
	 *
	 * @param array $errors
	 * @return bool True if rate-limited
	 */
	private function checkBatchErrorForRateLimit($errors) {
		$errorStr = is_array($errors) ? implode(' ', array_map(function($e) {
			return is_array($e) ? (isset($e['msg']) ? $e['msg'] : json_encode($e)) : (string)$e;
		}, $errors)) : (string)$errors;

		if ($this->isRateLimitError($errorStr)) {
			$this->activateImageBackoff('Error: ' . $errorStr);
			return true;
		}
		return false;
	}

	/**
	 * Set the rate-limit backoff transient to prevent Images API calls for 2 hours.
	 */
	private function activateImageBackoff($reason) {
		error_log('[Flow-Flow] LinkedIn Images API rate-limited, activating ' . (self::IMAGE_BACKOFF_TTL / 3600) . 'h backoff. ' . $reason);
		if (function_exists('set_transient')) {
			set_transient('ff_li_img_backoff', time(), self::IMAGE_BACKOFF_TTL);
		}
	}

	/**
	 * Map resolved image results back to post elements.
	 */
	private function mapImageResults(&$elements, $results) {
		if (empty((array) $results)) return;

		// Map back post images
		foreach ($elements as &$item) {
			if (isset($item->content->media->id)) {
				$urn = str_replace('digitalmediaAsset', 'image', $item->content->media->id);
				if (isset($results->{$urn}->downloadUrl)) {
					$item->content->media->originalUrl = $results->{$urn}->downloadUrl;
					if (isset($results->{$urn}->aspectRatioWidth) && isset($results->{$urn}->aspectRatioHeight)) {
						$item->content->media->width = (int) ($results->{$urn}->aspectRatioWidth * 1000);
						$item->content->media->height = (int) ($results->{$urn}->aspectRatioHeight * 1000);
					}
				}
			}
			if (isset($item->content->multiImage->images) && is_array($item->content->multiImage->images)) {
				foreach ($item->content->multiImage->images as &$img) {
					if (isset($img->id)) {
						$urn = str_replace('digitalmediaAsset', 'image', $img->id);
						if (isset($results->{$urn}->downloadUrl)) {
							$img->originalUrl = $results->{$urn}->downloadUrl;
							if (isset($results->{$urn}->aspectRatioWidth) && isset($results->{$urn}->aspectRatioHeight)) {
								$img->width = (int) ($results->{$urn}->aspectRatioWidth * 1000);
								$img->height = (int) ($results->{$urn}->aspectRatioHeight * 1000);
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Only show PUBLISHED posts.
	 */
	protected function isSuitableOriginalPost( $item ) {
		if (isset($item->lifecycleState) && $item->lifecycleState !== 'PUBLISHED') {
			return false;
		}
		return parent::isSuitableOriginalPost($item);
	}

	protected function getId( $item ) {
		// REST Posts return an `id` field like "urn:li:share:7012345678901234567"
		return isset($item->id) ? $item->id : '';
	}

	protected function getHeader( $item ) {
		// Articles embedded in the post may carry a title.
		if (isset($item->content->article->title)) {
			return $item->content->article->title;
		}
		return '';
	}

	protected function getScreenName( $item ) {
		return $this->orgName;
	}

	protected function getProfileImage( $item ) {
		return $this->profileImageUrl;
	}

	protected function getSystemDate( $item ) {
		// REST Posts use `createdAt` (millisecond epoch).
		if (isset($item->createdAt)) {
			return (int) floor($item->createdAt / 1000);
		}
		// Fallback: `publishedAt` or `lastModifiedAt`
		if (isset($item->publishedAt)) {
			return (int) floor($item->publishedAt / 1000);
		}
		return 0;
	}

	protected function getContent( $item ) {
		$content = '';

		// Primary text content
		if (!empty($item->commentary)) {
			$content = FFFeedUtils::wrapLinks($item->commentary);
		}

		// Article description (link preview)
		if (isset($item->content->article)) {
			$article = $item->content->article;
			if (!empty($article->title)) {
				$content .= '<br><br><b>' . htmlspecialchars($article->title, ENT_QUOTES, 'UTF-8') . '</b>';
			}
			if (!empty($article->description)) {
				$content .= '<br><br>' . htmlspecialchars($article->description, ENT_QUOTES, 'UTF-8');
			}
		}

		return $content;
	}

	protected function getUserlink( $item ) {
		return 'https://www.linkedin.com/company/' . $this->company;
	}

	protected function getPermalink( $item ) {
		// Build a public post URL from the share/ugcPost URN.
		// Format: urn:li:share:7012345678901234567 → activity ID
		$id = $this->getId($item);
		if (!empty($id)) {
			// Extract the numeric activity key from the URN
			$parts = explode(':', $id);
			$activityId = end($parts);
			if (is_numeric($activityId)) {
				return 'https://www.linkedin.com/feed/update/urn:li:activity:' . $activityId;
			}
		}
		return $this->getUserlink($item);
	}

	protected function showImage( $item ) {
		$url = $this->extractImageUrl($item);
		if (!empty($url)) {
			// Extract dimensions if mapped from the bulk resolution API
			$width = null;
			$height = null;
			if (isset($item->content->media->width)) {
				$width = $item->content->media->width;
				$height = $item->content->media->height;
			} elseif (isset($item->content->multiImage->images) && is_array($item->content->multiImage->images)) {
				$first = reset($item->content->multiImage->images);
				if (isset($first->width)) {
					$width = $first->width;
					$height = $first->height;
				}
			}

			$this->image = $this->createImage($url, $width, $height);
			$this->media = $this->createMedia($url, $width, $height);
			return true;
		}
		return false;
	}

	protected function getImage( $item ) {
		return $this->image;
	}

	protected function getMedia( $item ) {
		return $this->media;
	}

	protected function getAdditionalInfo( $item ) {
		$additional = parent::getAdditionalInfo( $item );
		// REST Posts surface social-action counts via separate API calls;
		// the Posts response itself does not embed like/comment counts.
		// We leave them at 0 and can enrich later via the socialActions API.
		$additional['likes']    = '0';
		$additional['comments'] = '0';
		return $additional;
	}

	// ------------------------------------------------------------------
	// Helpers
	// ------------------------------------------------------------------

	/**
	 * Try to extract an image URL from the post's content object.
	 *
	 * Supported media types in REST Posts:
	 *   - content.media (image/video — downloadUrl or original)
	 *   - content.article.thumbnail (link-preview thumbnail)
	 *   - content.multiImage.images[] (carousel of images)
	 *
	 * @param object $item
	 * @return string URL or empty string
	 */
	private function extractImageUrl( $item ) {

		// Single image / video thumbnail
		if (isset($item->content->media->id)) {
			// The `id` field is an asset URN; the downloadable URL is
			// sometimes surfaced in `originalUrl` or via the Images API.
			if (!empty($item->content->media->originalUrl)) {
				return $item->content->media->originalUrl;
			}
		}

		// Multi-image carousel — take first image
		if (isset($item->content->multiImage->images) && is_array($item->content->multiImage->images)) {
			$first = reset($item->content->multiImage->images);
			if (isset($first->id) && !empty($first->id)) {
				if (!empty($first->originalUrl)) {
					return $first->originalUrl;
				}
			}
		}

		// Article (link preview) thumbnail
		if (isset($item->content->article->thumbnail)) {
			return $item->content->article->thumbnail;
		}

		return '';
	}

	protected function isRateLimitError($message) {
		$throttlingMessages = [
			'Resource level throttle',
			'APPLICATION DAY limit',
			'Too Many Requests',
			'Throttled',
			'Rate limit reached'
		];
		foreach ($throttlingMessages as $msg) {
			if (stripos($message, $msg) !== false) return true;
		}
		return false;
	}
}
// phpcs:enable
