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

require_once __DIR__ . '/class-forge-crypto.php';

class RANKFORGE_Forge_API {
    private static $instance = null;
    private $api_url = 'https://api.avakode.com';
    private $license_key = '';
    private $site_token = '';
    private $trial_token = '';

    public static function instance() {
        if (self::$instance === null) self::$instance = new self();
        return self::$instance;
    }

    private function __construct() {
        // Allow wp-config override so local dev + staging can point at
        // their own Cloudflare Worker without touching source.
        if (defined('FORGE_API_URL') && FORGE_API_URL) {
            $this->api_url = rtrim(FORGE_API_URL, '/');
        }

        $data = get_option('rankforge_license_data', []);
        $this->license_key = $data['key'] ?? '';
        $this->site_token = Forge_Crypto::decrypt(isset($data['site_token']) ? (string) $data['site_token'] : '');
        $this->trial_token = Forge_Crypto::decrypt(isset($data['trial_token']) ? (string) $data['trial_token'] : '');

        // Fallback: use Freemius license key for real paying users
        if (empty($this->license_key) && function_exists('sf_freemius')) {
            $fs = sf_freemius();
            if ($fs && $fs->is_registered()) {
                $license = $fs->_get_license();
                if (is_object($license) && !empty($license->secret_key)) {
                    $this->license_key = $license->secret_key;
                }
            }
        }
    }

    public function validate_license($key = null) {
        $key = $key ?: $this->license_key;
        if (empty($key)) return ['valid' => false, 'plan' => 'free', 'features' => []];

        $response = $this->post('/licenses/validate', [
            'license_key' => $key,
            'product' => 'rankforge',
            'domain' => home_url('/'),
        ]);

        if (!is_wp_error($response) && is_array($response) && !empty($response['token'])) {
            $this->persist_site_token((string) $response['token'], $key);
        }

        return $response ?: ['valid' => false, 'plan' => 'free', 'features' => []];
    }

    private function persist_site_token($site_token, $license_key = '') {
        if ($site_token === '') return;
        $data = get_option('rankforge_license_data', []);
        if (!is_array($data)) $data = [];
        if ($license_key !== '') {
            $data['key'] = $license_key;
        }
        $data['site_token'] = Forge_Crypto::encrypt($site_token);
        $data['site_url'] = home_url('/');
        $data['site_token_refreshed_at'] = time();
        update_option('rankforge_license_data', $data, false);
        $this->site_token = $site_token;
    }

    /**
 * Ensure a per-site trial token exists (#39). Called on the first AI request
 * from a site with no license so the user gets free trial credits with no
 * setup. Best-effort.
 * @return bool True if a trial token is available afterwards.
 */
    public function maybe_start_trial() {
        if ($this->trial_token !== '') return true;
        if (!empty($this->license_key)) return false;
        $this->start_trial();
        return $this->trial_token !== '';
    }

    /**
 * Activate a trial against the Worker and persist the returned per-site
 * token (encrypted at rest). Defaults to the site admin email.
 * @param string $email Optional override; defaults to admin_email.
 * @return array|\WP_Error
 */
    public function start_trial($email = '') {
        $email = $email !== '' ? $email : (string) get_option('admin_email');
        $response = $this->post('/public/trial/activate', [
            'product' => 'rankforge',
            'domain'  => home_url('/'),
            'email'   => $email,
        ]);
        if (!is_wp_error($response) && is_array($response)
            && !empty($response['success']) && !empty($response['trial']['token'])) {
            $this->persist_trial_token((string) $response['trial']['token'], $response['trial']);
        }
        return $response;
    }

    private function persist_trial_token($token, $trial = []) {
        if ($token === '') return;
        $data = get_option('rankforge_license_data', []);
        if (!is_array($data)) $data = [];
        $data['trial_token'] = Forge_Crypto::encrypt($token);
        if (!empty($trial['expires_at'])) {
            $data['trial_expires_at'] = (string) $trial['expires_at'];
            $data['trial_expires'] = (string) $trial['expires_at'];
        }
        if (isset($trial['credits_remaining'])) $data['trial_credits_remaining'] = (int) $trial['credits_remaining'];
        if (isset($trial['credits_limit'])) $data['trial_credits_limit'] = (int) $trial['credits_limit'];
        update_option('rankforge_license_data', $data, false);
        $this->trial_token = $token;
    }

    private function clear_site_token() {
        $data = get_option('rankforge_license_data', []);
        if (!is_array($data)) $data = [];
        unset($data['site_token'], $data['site_token_refreshed_at']);
        update_option('rankforge_license_data', $data, false);
        $this->site_token = '';
    }

    private function refresh_site_token() {
        if ($this->license_key === '') return false;
        $response = $this->validate_license($this->license_key);
        if (is_wp_error($response)) {
            return $this->site_token !== '';
        }
        if (is_array($response) && !empty($response['token'])) {
            return true;
        }
        $this->clear_site_token();
        return false;
    }

    private function integration_auth_body($extra = []) {
        if ($this->site_token === '') {
            $this->refresh_site_token();
        }
        $body = [
            'license_key' => $this->license_key,
            'product' => 'rankforge',
            'forge_site_url' => home_url('/'),
        ];
        if ($this->site_token !== '') {
            $body['site_token'] = $this->site_token;
        }
        return array_merge($body, $extra);
    }

    public function ai_generate_meta($title, $content, $keywords = [], $language = '') {
        return $this->post('/ai/process', [
            'action' => 'generate_meta',
            'product' => 'rankforge',
            'license_key' => $this->license_key,
            'payload' => [
                'title' => $title,
                'content' => substr($content, 0, 20000),
                'keywords' => $keywords,
                'language' => $language,
                'domain' => $this->get_domain(),
            ],
        ]);
    }

    public function ai_generate_title($title, $content, $keyword = '', $language = '', $style = '') {
        return $this->post('/ai/process', [
            'action' => 'generate_title',
            'product' => 'rankforge',
            'license_key' => $this->license_key,
            'payload' => [
                'title' => $title,
                'content' => substr($content, 0, 20000),
                'keyword' => $keyword,
                'language' => $language,
                'style' => $style,
                'domain' => $this->get_domain(),
            ],
        ]);
    }

    public function ai_generate_description($title, $content, $keyword = '', $language = '') {
        return $this->post('/ai/process', [
            'action' => 'generate_description',
            'product' => 'rankforge',
            'license_key' => $this->license_key,
            'payload' => [
                'title' => $title,
                'content' => substr($content, 0, 20000),
                'keyword' => $keyword,
                'language' => $language,
                'domain' => $this->get_domain(),
            ],
        ]);
    }

    public function ai_generate_keyword($content) {
        return $this->post('/ai/process', [
            'action' => 'generate_keyword',
            'product' => 'rankforge',
            'license_key' => $this->license_key,
            'payload' => [
                'content' => substr($content, 0, 8000),
                'domain' => $this->get_domain(),
            ],
        ]);
    }

    public function ai_expand_content($content, $keyword = '') {
        return $this->post('/ai/process', [
            'action' => 'expand_content', 'product' => 'rankforge', 'license_key' => $this->license_key,
            'payload' => ['content' => substr($content, 0, 20000), 'keyword' => $keyword, 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_simplify_content($content, $is_english = true) {
        return $this->post('/ai/process', [
            'action' => 'simplify_content', 'product' => 'rankforge', 'license_key' => $this->license_key,
            // is_english lets the Worker apply our English-only Flesch gate (and
            // skip it for other languages, which the scorer auto-passes) so the
            // rewrite it returns is guaranteed to clear "Fairly hard to read".
            'payload' => ['content' => substr($content, 0, 20000), 'is_english' => (bool) $is_english, 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_add_external_links($content) {
        return $this->post('/ai/process', [
            'action' => 'add_external_links', 'product' => 'rankforge', 'license_key' => $this->license_key,
            'payload' => ['content' => substr($content, 0, 20000), 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_keyword_in_subheading($keyword, $subheading) {
        return $this->post('/ai/process', [
            'action' => 'keyword_in_subheading', 'product' => 'rankforge', 'license_key' => $this->license_key,
            'payload' => ['keyword' => $keyword, 'subheading' => $subheading, 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_generate_schema($title, $content) {
        return $this->post('/ai/process', [
            'action' => 'generate_schema', 'product' => 'rankforge', 'license_key' => $this->license_key,
            'payload' => ['title' => $title, 'content' => substr($content, 0, 8000), 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_generate_local_business($business_name, $city, $description, $url) {
        return $this->post('/ai/process', [
            'action' => 'generate_local_business', 'product' => 'rankforge', 'license_key' => $this->license_key,
            'payload' => ['business_name' => $business_name, 'city' => $city, 'description' => $description, 'url' => $url, 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_classify_query_intent($queries) {
        return $this->post('/ai/process', [
            'action' => 'classify_query_intent', 'product' => 'rankforge', 'license_key' => $this->license_key,
            'payload' => ['queries' => array_values((array) $queries), 'domain' => $this->get_domain()],
        ]);
    }

    public function ai_analyze_seo($url, $title, $content, $keyword = '') {
        return $this->post('/ai/process', [
            'action' => 'analyze_seo',
            'product' => 'rankforge',
            'license_key' => $this->license_key,
            'payload' => [
                'url' => $url,
                'title' => $title,
                'content' => substr($content, 0, 50000),
                'keyword' => $keyword,
                'domain' => $this->get_domain(),
            ],
        ]);
    }

    // The Fix button on issue rows called RANKFORGE_Forge_API::ai_fix_issue,
    // but the method didn't exist — every click produced a PHP fatal and the
    // REST endpoint returned 500, which the admin JS rendered as "Request failed".
    public function ai_fix_issue($content, $issue_message, $keyword = '', $is_english = true) {
        return $this->post('/ai/process', [
            'action'      => 'fix_issue',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'content'       => substr((string) $content, 0, 50000),
                'issue_message' => (string) $issue_message,
                'keyword'       => (string) $keyword,
                // For readability issues the Worker holds the rewrite to our
                // Flesch/sentence rules; is_english gates that (non-English
                // readability auto-passes in the scorer).
                'is_english'    => (bool) $is_english,
                'domain'        => $this->get_domain(),
            ],
        ]);
    }

    // Other AI client callers depend on these methods; they were also missing,
    // so any button that reaches them (internal-link suggest, auto-link insert,
    // AI readiness, content brief) would fail the same way.
    public function ai_suggest_links($title, $content, $candidates) {
        return $this->post('/ai/process', [
            'action'      => 'suggest_links',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'title'      => (string) $title,
                'content'    => substr((string) $content, 0, 30000),
                'candidates' => array_values((array) $candidates),
                'domain'     => $this->get_domain(),
            ],
        ]);
    }

    public function ai_auto_insert_links($content, $candidates) {
        return $this->post('/ai/process', [
            'action'      => 'auto_insert_links',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'content'    => substr((string) $content, 0, 50000),
                'candidates' => array_values((array) $candidates),
                'domain'     => $this->get_domain(),
            ],
        ]);
    }

    public function ai_analyze_readiness($title, $content) {
        return $this->post('/ai/process', [
            'action'      => 'analyze_readiness',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'title'   => (string) $title,
                'content' => substr((string) $content, 0, 50000),
                'domain'  => $this->get_domain(),
            ],
        ]);
    }

    // Generic "custom" AI call used by Local SEO auto-fill, Schema
    // Builder detection and the Content Optimizer. These callers were hitting
    // a non-existent RANKFORGE_AI_Client::call_public, so the request would
    // crash with an HTTP 500 and the UI stayed on "Generating…" forever.
    public function ai_custom($system_prompt, $user_prompt) {
        return $this->post('/ai/process', [
            'action'      => 'custom',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'system_prompt' => (string) $system_prompt,
                'user_prompt'   => (string) $user_prompt,
                'domain'        => $this->get_domain(),
            ],
        ]);
    }

    // dedicated endpoint for the Site Audit AI summary (was invoked
    // through a non-existent RANKFORGE_AI_Client::call_public before).
    public function ai_audit_summary($stats, $counts, $system_prompt = '') {
        return $this->post('/ai/process', [
            'action'      => 'audit_summary',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'stats'        => (array) $stats,
                'counts'       => (array) $counts,
                'system'       => (string) $system_prompt,
                'domain'       => $this->get_domain(),
            ],
        ]);
    }

    public function ai_generate_brief($keyword) {
        return $this->post('/ai/process', [
            'action'      => 'generate_brief',
            'product'     => 'rankforge',
            'license_key' => $this->license_key,
            'payload'     => [
                'keyword' => (string) $keyword,
                'domain'  => $this->get_domain(),
            ],
        ]);
    }

    // ─── Google Search Console ───────────────────────────────

    public function gsc_sites() {
        return $this->post('/integrations/google/gsc/sites', $this->integration_auth_body());
    }

    /**
 * Run a Search Analytics query against a user-owned GSC property.
 * `$params` is the body Google expects at
 * /webmasters/v3/sites/{site}/searchAnalytics/query — startDate,
 * endDate, dimensions, dimensionFilterGroups, rowLimit, dataState, etc.
 */
    public function gsc_search_analytics($site_url, array $params) {
        return $this->post('/integrations/google/gsc/search-analytics', $this->integration_auth_body([
            'site_url'    => (string) $site_url,
            'params'      => $params,
        ]));
    }

    /**
 * phase 2 — URL Inspection API.
 * Asks Google for the current indexing status of a single URL belonging
 * to an already-verified GSC property. Returns the full `inspectionResult`
 * envelope from Google's v1 URL Inspection API unchanged — higher-level
 * flattening happens in RANKFORGE_GSC::inspect_url.
 */
    public function gsc_url_inspection($site_url, $page_url) {
        return $this->post('/integrations/google/gsc/url-inspection', $this->integration_auth_body([
            'site_url'    => (string) $site_url,
            'page_url'    => (string) $page_url,
        ]));
    }

    /**
 * Check whether the current site's Google account is connected at
 * the forge-api level. Shared with Indexing — connecting once
 * authorizes webmasters.readonly + analytics.readonly + indexing +
 * drive.file + calendar.*, so is_connected tells us both GSC and
 * Indexing can talk to Google right now.
 */
    public function google_status() {
        return $this->post('/integrations/google/status', $this->integration_auth_body());
    }

    /**
 * Build the URL that kicks off the forge-api Google OAuth flow.
 * The Worker holds GOOGLE_CLIENT_SECRET, so the consent URL points
 * at `api.avakode.com/integrations/google/callback` regardless of
 * which WP site initiated it — matches the Google OAuth
 * verification scenario doc.
 */
    public function google_auth_url($return_url = null) {
        // Connect-flakiness fix: pass the WP admin URL the user should
        // land on after OAuth succeeds. The Worker's callback signs this
        // through OAuth state KV and 302s the user there directly, with
        // ?gsc_connected=1 appended so handle_post_connect busts the
        // connection-state transient on first paint.

        // Same-tab navigation (no target="_blank") means the user is sent
        // through OAuth in their existing tab and brought back to the
        // page they came from. Each page passes its own admin URL so the
        // user lands where they started; we default to Settings only
        // when the caller doesn't specify (safe fallback).
        if (empty($return_url)) {
            $return_url = admin_url('admin.php?page=rankforge-settings');
        }

        // Use the shared helper when the loaded Forge_Connect version has it.
        // On multi-plugin installs an older plugin can define Forge_Connect
        // first via the class_exists guard; method_exists keeps Settings from
        // fatalling and falls back to the locally cached token / validation.
        $token = $this->site_token;
        if (class_exists('Forge_Connect') && method_exists('Forge_Connect', 'get_or_refresh_site_token')) {
            $token = \Forge_Connect::get_or_refresh_site_token('rankforge', true);
        }
        if ($token === '' && $this->license_key !== '') {
            $response = $this->validate_license($this->license_key);
            if (is_array($response) && !empty($response['token'])) {
                $token = (string) $response['token'];
            }
        }

        // Sync instance state so integration_auth_body stays consistent.
        if ($token !== '') {
            $this->site_token = $token;
        }

        if ($token === '') {
            // No valid license — return '' so caller renders disabled button
            // with "Activate PRO license to connect Google" hint.
            return '';
        }

        // Forge_Connect::get_or_refresh_site_token resolves the
        // license key from the live Freemius license and persists it under
        // rankforge_license_data['key'] — but our singleton constructor may
        // have cached an empty key if Freemius synced after we booted. Re-read
        // the persisted key so the consent URL carries the real license_key
        // for Worker-side attribution (the site_token alone also identifies the
        // install, so an empty key here is a safe degrade rather than a break).
        if ($this->license_key === '') {
            $data = get_option('rankforge_license_data', []);
            if (is_array($data) && !empty($data['key'])) {
                $this->license_key = (string) $data['key'];
            }
        }

        // Mint a one-shot init token server-side so the license_key +
        // site_token NEVER ride in the browser URL (review #HIGH). The
        // browser only ever navigates to the opaque ?init=<token>.
        $init = $this->mint_google_init_token([
            'license_key' => $this->license_key,
            'product'     => 'rankforge',
            'site_url'    => home_url('/'),
            'site_token'  => $token,
            'return_url'  => $return_url,
        ]);
        if ($init === '') {
            return '';
        }

        return $this->api_url . '/integrations/google/auth?init=' . rawurlencode($init);
    }

    /**
 * Mint a one-shot Google OAuth `init` token from the Worker.
 *
 * The license_key + site_token are POSTed in the request BODY, so they
 * never appear in the browser URL / history / Referer (review #HIGH,
 * 2026-06-07). Returns '' on any failure so the caller degrades to a
 * disabled "Connect Google" button instead of a broken link.
 *
 * @param array $fields license_key, product, site_url, site_token, return_url
 * @return string opaque init token, or '' on failure
 */
    private function mint_google_init_token($fields) {
        $response = wp_remote_post($this->api_url . '/integrations/google/auth/init', [
            'timeout' => 10,
            'headers' => ['Content-Type' => 'application/json'],
            'body'    => wp_json_encode($fields),
        ]);
        if (is_wp_error($response)) {
            return '';
        }
        if ((int) wp_remote_retrieve_response_code($response) !== 200) {
            return '';
        }
        $data = json_decode(wp_remote_retrieve_body($response), true);
        if (is_array($data) && !empty($data['init'])) {
            return (string) $data['init'];
        }
        return '';
    }

    public function google_disconnect() {
        return $this->post('/integrations/google/disconnect', $this->integration_auth_body());
    }

    // ─── Google Analytics 4 ──────────────────────────────────

    public function ga4_properties() {
        return $this->post('/integrations/google/ga4/properties', $this->integration_auth_body());
    }

    /**
 * Run a GA Data API runReport. `$params` mirrors the body Google
 * expects at /v1beta/properties/{id}:runReport.
 */
    public function ga4_report($property_id, array $params) {
        return $this->post('/integrations/google/ga4/report', $this->integration_auth_body([
            'property_id' => (string) $property_id,
            'params'      => $params,
        ]));
    }

    // ─── Google Indexing API ─────────────────────────────────

    public function indexing_submit($url, $type = 'URL_UPDATED') {
        return $this->post('/integrations/google/indexing/submit', $this->integration_auth_body([
            'url'         => (string) $url,
            'type'        => (string) $type,
        ]));
    }

    public function indexing_history($limit = 50) {
        return $this->post('/integrations/google/indexing/history', $this->integration_auth_body([
            'limit'       => (int) $limit,
        ]));
    }

    private function post($endpoint, $body) {
        $is_ai = strpos($endpoint, '/ai/') === 0;
        // Trial activation (#39) is started explicitly via the "Start free
        // trial" button in the shared credits widget now, not auto-started on
        // the first AI call. We still attach an existing trial token below.
        $headers = [
            'Content-Type' => 'application/json',
            'X-Site-URL' => $this->get_domain(),
        ];
        if ($is_ai && empty($this->license_key) && $this->trial_token !== '') {
            $headers['X-Trial-Token'] = $this->trial_token;
        }
        $start = microtime(true);
        $response = wp_remote_post($this->api_url . $endpoint, [
            'timeout' => 60,
            'headers' => $headers,
            'body' => wp_json_encode($body),
        ]);
        $duration_ms = (int) round((microtime(true) - $start) * 1000);
        $action = isset($body['action']) ? (string) $body['action'] : '';

        if (is_wp_error($response)) {
            self::telemetry_error($endpoint, $action, 0, 'network', $response->get_error_message());
            return $response;
        }

        $code = wp_remote_retrieve_response_code($response);
        $data = json_decode(wp_remote_retrieve_body($response), true);

        if ($code === 402 && isset($data['error']) && $data['error'] === 'credits_exhausted') {
            self::telemetry_error($endpoint, $action, 402, 'credits_exhausted', $data['error'] ?? '');
            return new \WP_Error('credits_exhausted', __('Monthly AI credits exhausted.', 'rankforge'), [
                'status' => 402,
                'remaining' => $data['remaining'] ?? 0,
                'reset_date' => $data['reset_date'] ?? '',
            ]);
        }

        // Push fresh credit balance to widget cache so all 4 plugin chips
        // reflect the deduction immediately (bundle = shared pool).
        if ($code < 400 && strpos($endpoint, '/ai/process') !== false
            && is_array($data) && isset($data['credits'])
            && class_exists('Forge_Credits_Widget')) {
            \Forge_Credits_Widget::push_credits($data['credits']);
        }

        if ($code >= 400) {
            $err_msg = is_array($data) ? (string) ($data['error'] ?? 'API error') : 'API error';
            $err_class = self::classify_error($code, $err_msg);
            self::telemetry_error($endpoint, $action, $code, $err_class, $err_msg);
            return new \WP_Error('forge_api_error', $err_msg, ['status' => $code]);
        }

        if (class_exists('Forge_Telemetry') && strpos($endpoint, '/ai/') !== false) {
            Forge_Telemetry::track('ai.request', [
                'endpoint'        => $endpoint,
                'action'          => $action,
                'status_code'     => $code,
                'duration_bucket' => self::bucket_duration($duration_ms),
                'success'         => true,
            ]);
        }

        return $data;
    }

    private static function bucket_duration(int $ms): string {
        if ($ms < 500)    return '0-500';
        if ($ms < 2000)   return '500-2k';
        if ($ms < 5000)   return '2k-5k';
        if ($ms < 15000)  return '5k-15k';
        if ($ms < 60000)  return '15k-60k';
        return '60k+';
    }
    private static function classify_error(int $code, string $msg): string {
        if ($code === 401 || $code === 403) return 'auth';
        if ($code === 429) return 'rate_limit';
        if ($code === 402) return 'credits_exhausted';
        if ($code >= 500)  return 'upstream_5xx';
        if (stripos($msg, 'country') !== false && stripos($msg, 'not supported') !== false) return 'geo_block';
        if ($code >= 400)  return 'client_4xx';
        return 'other';
    }
    private static function telemetry_error(string $endpoint, string $action, int $code, string $err_class, string $msg): void {
        if (!class_exists('Forge_Telemetry')) return;
        Forge_Telemetry::track_error('forge_api', [
            'endpoint'    => $endpoint,
            'action'      => $action,
            'status_code' => $code,
            'error_class' => $err_class,
            'message'     => $msg,
            // 2026-04-27 alert investigation: per-event plugin attribution
            // — without this every Forge_API error gets tagged with the
            // first-loaded plugin slug (langforge) by the shared telemetry
            // batch. See class-forge-telemetry.php detect_plugin_slug.
            'plugin_slug' => 'rankforge',
        ]);
    }

    private function get_domain() {
        return wp_parse_url(home_url(), PHP_URL_HOST);
    }

    public function get_license_key() { return $this->license_key; }
    public function set_license_key($key) { $this->license_key = $key; }
}
