<?php namespace flow\social;

use Exception;
use stdClass;

if ( ! defined( 'WPINC' ) ) die;

/**
 * Flow-Flow TikTok Integration.
 *
 * @package   FlowFlow
 * @author    Looks Awesome <email@looks-awesome.com>
 * @link      http://looks-awesome.com
 * @copyright 2014-2025 Looks Awesome
 */
class FFTikTok extends FFBaseFeed {
    
    private $accessToken;
    private $apiBaseUrl = 'https://open.tiktokapis.com/v2/';
    private $nextCursor = null;
    private $userInfo = null; // cache per-request user info
    
    public function __construct() {
        parent::__construct('tiktok');
    }
    
    /**
     * Refresh the TikTok token using the external service
     * @param string $refreshToken The refresh token to use
     * @param string $backUrl The URL to return to after refresh
     * @return array|false Array with new token data or false on failure
     */
    public function refreshToken($refreshToken, $backUrl) {
        if (empty($refreshToken)) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok] No refresh token provided');
            return false;
        }
        
        $refreshUrl = 'https://flow.looks-awesome.com/service/auth/callback/tiktok-auth.php?' . http_build_query([
            'back' => $backUrl,
            'refresh_token' => $refreshToken,
            'cron' => '1' // Indicate this is a cron request
        ]);
        
        $response = wp_remote_get($refreshUrl, [
            'timeout' => 30,
            'sslverify' => true,
            'headers' => [
                'User-Agent' => 'Flow-Flow-WordPress/1.0',
                'Accept' => 'application/json'
            ]
        ]);
        
        if (is_wp_error($response)) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok] Token refresh failed: ' . $response->get_error_message());
            return false;
        }
        
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        
        if (empty($data) || !is_array($data) || !empty($data['error'])) {
            $error = !empty($data['error']) ? $data['error'] : 'Invalid response from token service';
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok] Token refresh error: ' . $error);
            return false;
        }
        
        return $data;
    }
    
    /**
     * Initialize TikTok API settings
     * @param stdClass $feed
     * @throws Exception
     */
    public function deferredInit($feed) {
        // Support both per-feed token field names
        $token = null;
        if (isset($feed->tiktok_access_token) && !empty($feed->tiktok_access_token)) {
            $token = $feed->tiktok_access_token;
        } elseif (isset($feed->access_token) && !empty($feed->access_token)) {
            $token = $feed->access_token;
        }

        // Fallback to global token from options if per-feed token not provided
        if (empty($token)) {
            if (is_array($this->context) && isset($this->context['options']) && is_array($this->context['options'])) {
                if (!empty($this->context['options']['tiktok_access_token'])) {
                    $token = $this->context['options']['tiktok_access_token'];
                }
            }
        }

        if (empty($token)) {
            throw new LASocialException('TikTok access token is required', ['type' => 'tiktok']);
        }

        $this->accessToken = $token;
        
        // Validate access token by making a test request
        $this->validateAccessToken();
        
        // Force fetch and save user info when initializing with a token
        // if (is_admin() && !empty($this->context['plugin_slug']) && !empty($this->context['slug_add_down_vote'])) {
            // $this->getUserInfo();
        // }
    }
    
    /**
     * Validate the TikTok access token
     * @throws LASocialException
     */
    private function validateAccessToken() {
        // Use the same endpoint and fields as getUserInfo to validate token
        $url = $this->apiBaseUrl . 'user/info/?fields=open_id,username';
        $headers = [
            'Authorization: Bearer ' . $this->accessToken,
            'Content-Type: application/json'
        ];

        try {
            $response = $this->makeApiRequest($url, $headers);
            if (!$response || (isset($response['error']) && !empty($response['error']))) {
                throw new LASocialException('Invalid TikTok access token', ['type' => 'tiktok']);
            }
        } catch (LASocialRequestException $e) {
            // Consider 401/403 as invalid token, other HTTP errors we don't block feed creation
            $errors = $e->getRequestErrors();
            $httpCode = is_array($errors) && isset($errors['http_code']) ? intval($errors['http_code']) : 0;
            if ($httpCode === 401 || $httpCode === 403) {
                throw new LASocialException('Invalid TikTok access token 401', ['type' => 'tiktok']);
            }
            // Log and continue for other codes (e.g., 5xx temporary issues)
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok] Token validation skipped due to HTTP ' . $httpCode);
        } catch (Exception $e) {
            // Any other exception: log and continue to avoid blocking feed creation
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok] Token validation error: ' . $e->getMessage());
        }
    }
    
    /**
     * Fetch connected user's info (username, display name, avatar) and cache it for this request
     * @return array|null
     */
    private function getUserInfo() {
        if (is_array($this->userInfo)) return $this->userInfo;
        // Prefer cached values from options
        $opt = (is_array($this->context) && isset($this->context['options']) && is_array($this->context['options'])) ? $this->context['options'] : [];
        $optUsername     = isset($opt['tiktok_username']) ? trim($opt['tiktok_username']) : '';
        $optDisplayName  = isset($opt['tiktok_display_name']) ? trim($opt['tiktok_display_name']) : '';
        $optOpenId       = isset($opt['tiktok_open_id']) ? trim($opt['tiktok_open_id']) : '';
        $optUserpic      = isset($opt['tiktok_userpic']) ? trim($opt['tiktok_userpic']) : '';
        if ($optUsername !== '' || $optDisplayName !== '' || $optOpenId !== '' || $optUserpic !== '') {
            $this->userInfo = [
                'open_id'      => $optOpenId,
                'username'     => $optUsername,
                'display_name' => $optDisplayName,
                'avatar_url'   => $optUserpic,
            ];
            return $this->userInfo;
        }
        $url = $this->apiBaseUrl . 'user/info/?fields=open_id,display_name,username,avatar_url,avatar_url_100,avatar_large_url';
        $headers = [
            'Authorization: Bearer ' . $this->accessToken,
            'Content-Type: application/json'
        ];
        try {
            $resp = $this->makeApiRequest($url, $headers);
            if (isset($resp['data']['user']) && is_array($resp['data']['user'])) {
                $u = $resp['data']['user'];
                // normalize avatar url
                $avatar = '';
                if (!empty($u['avatar_url'])) $avatar = $u['avatar_url'];
                elseif (!empty($u['avatar_url_100'])) $avatar = $u['avatar_url_100'];
                elseif (!empty($u['avatar_large_url'])) $avatar = $u['avatar_large_url'];
                $this->userInfo = [
                    'open_id'      => isset($u['open_id']) ? $u['open_id'] : '',
                    'username'     => isset($u['username']) ? $u['username'] : '',
                    'display_name' => isset($u['display_name']) ? $u['display_name'] : '',
                    'avatar_url'   => $avatar,
                ];
                
                // Update options with user info if this is an admin context
                if (is_admin() && !empty($this->context['plugin_slug']) && !empty($this->context['slug_add_down_vote']) && $this->context['slug_add_down_vote'] === 'flow_flow_options') {
                    $options = get_option($this->context['plugin_slug'] . '_' . $this->context['slug_add_down_vote'], []);
                    if (is_array($options)) {
                        $options['tiktok_username'] = $this->userInfo['username'];
                        $options['tiktok_display_name'] = $this->userInfo['display_name'];
                        $options['tiktok_open_id'] = $this->userInfo['open_id'];
                        $options['tiktok_userpic'] = $this->userInfo['avatar_url'];
                        update_option($this->context['plugin_slug'] . '_' . $this->context['slug_add_down_vote'], $options);
                    }
                }
                return $this->userInfo;
            }
        } catch (Exception $e) {
            // ignore, fallback to defaults
        }
        $this->userInfo = null;
        return null;
    }
    
    /**
     * Fetch one page of TikTok posts
     * @return array
     * @throws LASocialException
     * @throws Exception
     */
    public function onePagePosts() {
        $result = [];
        
        try {
            // Ensure we have latest user info for username/avatar per each page fetch
            $user = $this->getUserInfo();
            $fields = 'id,create_time,cover_image_url,share_url,video_description,duration,height,width,title,embed_html,embed_link,like_count,comment_count,share_count,view_count';
            // Some TikTok environments require `fields` to be present in the query string, even for POST
            $url = $this->apiBaseUrl . 'video/list/?fields=' . rawurlencode($fields);
            $payload = [
                'max_count' => min($this->getCount(), 20),
                // Keep fields in body as well for maximum compatibility
                'fields' => $fields
            ];

            if ($this->nextCursor) {
                $payload['cursor'] = $this->nextCursor;
            }

            $headers = [
                'Authorization: Bearer ' . $this->accessToken,
                'Content-Type: application/json'
            ];
            
            // TikTok Open API v2 uses POST for video.list
            $response = $this->makeApiRequest($url, $headers, 'POST', json_encode($payload));
            
            if (!$response) {
                throw new LASocialException('Failed to fetch TikTok posts', ['type' => 'tiktok']);
            }
            // Some TikTok responses include an `error` object even on success with code "ok"
            if (isset($response['error'])) {
                $err = $response['error'];
                $code = isset($err['code']) ? strtolower((string)$err['code']) : '';
                if ($code !== '' && $code !== 'ok') {
                    $errorMsg = isset($err['message']) && $err['message'] !== '' ? $err['message'] : 'Failed to fetch TikTok posts';
                    throw new LASocialException($errorMsg, ['type' => 'tiktok']);
                }
                // If code is 'ok', proceed as a successful response
            }
            
            if (isset($response['data']['videos']) && is_array($response['data']['videos'])) {
                foreach ($response['data']['videos'] as $video) {
                    $post = $this->parseVideoToPost($video);
                    // enrich with user info if available
                    if ($post && is_array($user)) {
                        // Always use connected user for labels
                        $uname = isset($user['username']) ? trim($user['username']) : '';
                        $dname = isset($user['display_name']) ? trim($user['display_name']) : '';
                        if ($uname !== '') {
                            $post->nickname = '@' . ltrim($uname, '@');
                            $post->userlink = 'https://www.tiktok.com/@' . ltrim($uname, '@');
                        } else {
                            // Fallback: show display name as nickname when no username
                            if ($dname !== '') $post->nickname = $dname;
                            $post->userlink = '';
                        }
                        if ($dname !== '') {
                            $post->screenname = $dname;
                        } elseif ($post->nickname === '') {
                            $post->screenname = 'TikTok';
                        }
                        if (!empty($user['avatar_url'])) {
                            $post->userpic = $user['avatar_url'];
                        }
                    }
                    if ($post && $this->isSuitablePost($post)) {
                        $result[$post->id] = $post;
                    }
                }
                
                // Set cursor for next page
                if (isset($response['data']['cursor']) && isset($response['data']['has_more']) && $response['data']['has_more']) {
                    $this->nextCursor = $response['data']['cursor'];
                } else {
                    $this->nextCursor = null;
                }
            }
            
        } catch (LASocialRequestException $sre) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log($sre->getMessage());
            $request_errors = $sre->getRequestErrors();
            $message = is_object($request_errors) ? 'Error getting data from TikTok server' : $this->filterErrorMessage($request_errors);
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
            throw new LASocialException(function_exists('esc_html') ? esc_html($message) : htmlspecialchars($message, ENT_QUOTES, 'UTF-8'), ['type' => 'tiktok']);
        }
        
        return $result;
    }
    
    /**
     * Parse TikTok video data into standard post format
     * @param array $video
     * @return stdClass|null
     */
    private function parseVideoToPost($video) {
        if (!isset($video['id'])) {
            return null;
        }
        
        $post = new stdClass();
        $post->id = $video['id'];
        $post->feed_id = $this->id();
        $post->type = $this->getType();
        $post->smart_order = 0;
        
        // Basic info
        $post->text = isset($video['video_description']) ? $video['video_description'] : '';
        $post->header = isset($video['title']) ? mb_substr($video['title'], 0, 200) : '';
        $post->permalink = isset($video['share_url']) ? $video['share_url'] : '';
        
        // Timestamp
        $post->system_timestamp = isset($video['create_time']) ? $video['create_time'] : time();
        
        // User info will be injected from connected user in onePagePosts()
        $post->nickname = '';
        $post->screenname = '';
        $post->userpic = '';
        $post->userlink = '';
        
        // Media
        if (isset($video['cover_image_url'])) {
            $width = isset($video['width']) ? $video['width'] : 720;
            $height = isset($video['height']) ? $video['height'] : 1280;
            $post->img = $this->createImage($video['cover_image_url'], $width, $height);

            // Try to resolve a direct MP4 URL server-side; fallback to share/embed link
            $mp4Url = $this->resolveDirectMp4FromVideo($video);
            if (is_string($mp4Url) && $mp4Url !== '') {
                // native <video> path in public.js via mediaType 'video/mp4'
                $post->media = $this->createMedia($mp4Url, $width, $height, 'video/mp4');
            } else {
                // fallback to iframe-capable URL
                $fallback = '';
                if (!empty($video['embed_link'])) $fallback = $video['embed_link'];
                elseif (!empty($video['share_url'])) $fallback = $video['share_url'];
                if ($fallback !== '') {
                    // mark as text/html so frontend treats it as an embeddable URL
                    $post->media = $this->createMedia($fallback, $width, $height, 'text/html');
                } else {
                    $post->media = $post->img;
                }
            }
        }
        
        // Additional stats
        $post->additional = [];
        if (isset($video['like_count'])) {
            $post->additional['likes'] = (string)$video['like_count'];
        }
        if (isset($video['comment_count'])) {
            $post->additional['comments'] = (string)$video['comment_count'];
        }
        if (isset($video['share_count'])) {
            $post->additional['shares'] = (string)$video['share_count'];
        }
        if (isset($video['view_count'])) {
            $post->additional['views'] = (string)$video['view_count'];
        }
        
        $post->carousel = [];
        
        return $post;
    }
    
    /**
     * Make API request to TikTok
     * @param string $url
     * @param array $headers
     * @return array|null
     * @throws LASocialRequestException
     */
    private function makeApiRequest($url, $headers = [], $method = 'GET', $body = null) {
        // phpcs:disable WordPress.WP.AlternativeFunctions
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Flow-Flow TikTok Integration/1.0');
        
        if (strtoupper($method) === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            if (!is_null($body)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
            }
        }
        
        // Conditional debug logging (no secrets)
        $debug = defined('WP_DEBUG') && WP_DEBUG;
        if ($debug) {
            // Sanitize headers (mask Authorization token)
            $safeHeaders = [];
            foreach ((array)$headers as $h) {
                if (stripos($h, 'authorization:') === 0) {
                    $safeHeaders[] = 'Authorization: Bearer ***';
                } else {
                    $safeHeaders[] = $h;
                }
            }
            $logBody = '';
            if (!is_null($body)) {
                $logBody = is_string($body) ? $body : json_encode($body);
                if (strlen($logBody) > 512) $logBody = substr($logBody, 0, 512) . '...';
            }
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok][REQ] ' . strtoupper($method) . ' ' . $url . ' | headers=' . json_encode($safeHeaders) . ' | body=' . $logBody);
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        // phpcs:enable WordPress.WP.AlternativeFunctions
        
        if ($error) {
            if ($debug) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                error_log('[Flow-Flow TikTok][ERR] cURL error for ' . $url . ': ' . $error);
            }
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
            throw new LASocialRequestException($url, ['curl_error' => $error]);
        }
        
        if ($httpCode !== 200) {
            if ($debug) {
                $snippet = is_string($response) ? substr($response, 0, 512) : '';
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                error_log('[Flow-Flow TikTok][RES] HTTP ' . $httpCode . ' for ' . $url . ' | body=' . $snippet);
            }
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
            throw new LASocialRequestException($url, ['http_code' => $httpCode, 'response' => $response]);
        }
        
        $data = json_decode($response, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            if ($debug) {
                $snippet = is_string($response) ? substr($response, 0, 256) : '';
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                error_log('[Flow-Flow TikTok][ERR] JSON decode failed for ' . $url . ': ' . json_last_error_msg() . ' | body=' . $snippet);
            }
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
            throw new LASocialRequestException($url, ['json_error' => json_last_error_msg()]);
        }
        if ($debug) {
            // Log small subset of response for debugging
            $respSnippet = substr(json_encode($data), 0, 512);
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[Flow-Flow TikTok][OK ] ' . $url . ' | body=' . $respSnippet);
        }
        
        return $data;
    }
    
    /**
     * Attempt to resolve a direct, short-lived MP4 URL for a TikTok item using public web JSON and redirects.
     * Returns empty string on failure.
     * @param array $video
     * @return string
     */
    private function resolveDirectMp4FromVideo($video) {
        $debug = defined('WP_DEBUG') && WP_DEBUG;
        try {
            $itemId = '';
            if (!empty($video['id'])) $itemId = (string)$video['id'];
            // Try to extract from share_url if needed
            if ($itemId === '' && !empty($video['share_url'])) {
                if (preg_match('~/(video|v)/(\d+)~', $video['share_url'], $m)) {
                    $itemId = $m[2];
                }
            }
            if ($itemId === '') return '';

            // Fetch item detail from web API
            $detailUrl = 'https://www.tiktok.com/api/item/detail/?itemId=' . rawurlencode($itemId);
            $headers = [
                'User-Agent: Mozilla/5.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36',
                'Referer: https://www.tiktok.com/',
                'Accept-Language: en-US,en;q=0.9'
            ];
            $detail = $this->httpGetJson($detailUrl, $headers);
            if (!is_array($detail)) return '';

            // Probe multiple possible JSON paths for play URL candidates
            $candidates = [];
            $it = isset($detail['itemInfo']['itemStruct']) ? $detail['itemInfo']['itemStruct'] : null;
            if (is_array($it) && isset($it['video']) && is_array($it['video'])) {
                $v = $it['video'];
                if (isset($v['bitRate']) && is_array($v['bitRate'])) {
                    foreach ($v['bitRate'] as $br) {
                        if (isset($br['playAddr']['urlList']) && is_array($br['playAddr']['urlList'])) {
                            foreach ($br['playAddr']['urlList'] as $u) $candidates[] = $u;
                        }
                    }
                }
                if (isset($v['playAddr']['urlList']) && is_array($v['playAddr']['urlList'])) {
                    foreach ($v['playAddr']['urlList'] as $u) $candidates[] = $u;
                }
                if (empty($candidates) && !empty($v['vid'])) {
                    $candidates[] = 'https://api16-normal-c-useast2a.tiktokv.com/aweme/v1/play/?video_id=' . rawurlencode($v['vid']) . '&is_play_url=1&line=0';
                }
            }

            foreach ($candidates as $cand) {
                $final = $this->resolveFinalUrl($cand, $headers);
                if ($final !== '' && preg_match('~https?://v\d+\.tiktokcdn\.com/.*~', $final)) {
                    return $final;
                }
            }
        } catch (\Exception $e) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            if ($debug) error_log('[Flow-Flow TikTok][MP4] resolver error: ' . $e->getMessage());
        }
        return '';
    }

    /**
     * Perform a GET expecting JSON response. Simple helper with custom headers.
     * @param string $url
     * @param array $headers
     * @return array|null
     */
    private function httpGetJson($url, $headers = []) {
        // phpcs:disable WordPress.WP.AlternativeFunctions
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        if (!empty($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $resp = curl_exec($ch);
        $err = curl_error($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        // phpcs:enable WordPress.WP.AlternativeFunctions
        if ($err || $code !== 200) return null;
        $data = json_decode($resp, true);
        return (json_last_error() === JSON_ERROR_NONE) ? $data : null;
    }

    /**
     * Follow redirects and return the final effective URL without downloading the body.
     * @param string $url
     * @param array $headers
     * @return string
     */
    private function resolveFinalUrl($url, $headers = []) {
        // phpcs:disable WordPress.WP.AlternativeFunctions
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        if (!empty($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_exec($ch);
        $final = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        curl_close($ch);
        // phpcs:enable WordPress.WP.AlternativeFunctions
        return is_string($final) ? $final : '';
    }
    
    /**
     * Check if there are more pages to fetch
     * @param array $result
     * @return bool
     */
    protected function nextPage($result) {
        return !is_null($this->nextCursor) && count($result) < $this->getCount();
    }
}
