<?php
namespace Consolety;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Campaigns class - Article Marketplace
 *
 * Pro users can create campaigns (ready-to-publish articles)
 * All users (Free + Pro) can browse and claim campaigns
 */
class Campaigns {

    private $api;

    public function __construct(Api $api) {
        $this->api = $api;

        // AJAX handlers for campaigns
        add_action('wp_ajax_consolety_get_campaigns', [$this, 'ajax_get_campaigns']);
        add_action('wp_ajax_consolety_get_my_campaigns', [$this, 'ajax_get_my_campaigns']);
        add_action('wp_ajax_consolety_get_my_claims', [$this, 'ajax_get_my_claims']);
        add_action('wp_ajax_consolety_get_campaign', [$this, 'ajax_get_campaign']);
        add_action('wp_ajax_consolety_create_campaign', [$this, 'ajax_create_campaign']);
        add_action('wp_ajax_consolety_claim_campaign', [$this, 'ajax_claim_campaign']);
        add_action('wp_ajax_consolety_accept_claim', [$this, 'ajax_accept_claim']);
        add_action('wp_ajax_consolety_reject_claim', [$this, 'ajax_reject_claim']);
        add_action('wp_ajax_consolety_withdraw_campaign', [$this, 'ajax_withdraw_campaign']);
        add_action('wp_ajax_consolety_publish_campaign', [$this, 'ajax_publish_campaign']);
        add_action('wp_ajax_consolety_dismiss_campaign', [$this, 'ajax_dismiss_campaign']);
        add_action('wp_ajax_consolety_dismiss_claim', [$this, 'ajax_dismiss_claim']);
    }

    /**
     * Render campaigns page
     */
    public function render_page() {
        include CONSOLETY_PLUGIN_DIR . 'admin/views/campaigns.php';
    }

    // ==========================================
    // AJAX Handlers
    // ==========================================

    /**
     * AJAX: Get all open campaigns
     */
    public function ajax_get_campaigns() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $response = $this->api->getCampaigns();

        if (!empty($response['success'])) {
            wp_send_json_success($response['campaigns'] ?? []);
        }

        // Structured error response with code + message
        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to load campaigns'
        ]);
    }

    /**
     * AJAX: Get my campaigns (as creator)
     */
    public function ajax_get_my_campaigns() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $response = $this->api->getMyCampaigns();

        if (!empty($response['success'])) {
            wp_send_json_success($response['campaigns'] ?? []);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to load campaigns'
        ]);
    }

    /**
     * AJAX: Get my claims (as publisher)
     */
    public function ajax_get_my_claims() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $response = $this->api->getMyClaims();

        if (!empty($response['success'])) {
            wp_send_json_success($response['claims'] ?? []);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to load claims'
        ]);
    }

    /**
     * AJAX: Get single campaign with details
     */
    public function ajax_get_campaign() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);
        if (!$campaignId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid campaign ID']);
        }

        $response = $this->api->getCampaign($campaignId);

        if (!empty($response['success'])) {
            wp_send_json_success($response['campaign'] ?? []);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to load campaign'
        ]);
    }

    /**
     * AJAX: Create new campaign
     */
    public function ajax_create_campaign() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        // Validate input
        // WordPress slashes superglobals; unslash first to avoid storing escaped quotes like \'
        $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
        $content = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
        $targetUrl = esc_url_raw(wp_unslash($_POST['target_url'] ?? ''));
        $imageUrl = esc_url_raw(wp_unslash($_POST['image_url'] ?? ''));

        // Filters
        $filterCategories = isset($_POST['filter_categories']) ? array_map('absint', (array)$_POST['filter_categories']) : null;

        // Locale: combined language + country code (e.g., "en_US", "nl_NL")
        $locale = sanitize_text_field(wp_unslash($_POST['locale'] ?? ''));

        if (empty($title)) {
            wp_send_json_error(['code' => 'VALIDATION_ERROR', 'message' => __('Title is required', 'consolety')]);
        }

        $plainText = wp_strip_all_tags($content);
        if (str_word_count($plainText) < 100) {
            wp_send_json_error(['code' => 'VALIDATION_ERROR', 'message' => __('Content must be at least 100 words', 'consolety')]);
        }

        if (empty($locale)) {
            wp_send_json_error(['code' => 'VALIDATION_ERROR', 'message' => __('Please select a language and target market', 'consolety')]);
        }

        // Validate locale format (xx_XX)
        if (!preg_match('/^[a-z]{2}_[A-Z]{2}$/', $locale)) {
            wp_send_json_error(['code' => 'VALIDATION_ERROR', 'message' => __('Invalid locale format', 'consolety')]);
        }

        $data = [
            'title' => $title,
            'content' => $content,
            'locale' => $locale,
        ];

        if (!empty($targetUrl)) {
            $data['target_url'] = $targetUrl;
        }
        if (!empty($imageUrl)) {
            $data['image_url'] = $imageUrl;
        }
        if (!empty($filterCategories)) {
            $data['filter_categories'] = $filterCategories;
        }

        $response = $this->api->createCampaign($data);

        if (!empty($response['success'])) {
            wp_send_json_success([
                'message' => __('Campaign created successfully! 30 points have been reserved.', 'consolety'),
                'campaign' => $response['campaign'] ?? null,
            ]);
        }

        // Handle specific errors
        $errorCode = $response['error']['code'] ?? ($response['error'] ?? 'UNKNOWN');
        $errorMessage = $response['error']['message'] ?? $response['error'] ?? __('Failed to create campaign', 'consolety');

        if (is_string($errorCode)) {
            switch ($errorCode) {
                case 'INSUFFICIENT_POINTS':
                    $errorMessage = __('Not enough points. You need 30 points to create a campaign.', 'consolety');
                    break;
                case 'CAMPAIGN_LIMIT':
                    $errorMessage = __('You have reached the campaign limit for your current plan.', 'consolety');
                    break;
            }
        }

        wp_send_json_error(['code' => $errorCode, 'message' => $errorMessage]);
    }

    /**
     * AJAX: Claim a campaign
     */
    public function ajax_claim_campaign() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);
        $pitch = sanitize_textarea_field(wp_unslash($_POST['pitch'] ?? ''));

        if (!$campaignId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid campaign ID']);
        }

        // Idempotency: reuse the same key for retries of the same campaign claim.
        // If a previous attempt's response was lost (network drop), the retry with
        // the same key returns the existing claim instead of ALREADY_CLAIMED.
        $transientKey = 'vp_claim_idem_' . $campaignId;
        $idempotencyKey = get_transient($transientKey);
        if (!$idempotencyKey) {
            $idempotencyKey = wp_generate_uuid4();
            set_transient($transientKey, $idempotencyKey, 600); // 10 min window
        }

        $response = $this->api->claimCampaign($campaignId, $pitch, $idempotencyKey);

        if (!empty($response['success'])) {
            delete_transient($transientKey); // Claim succeeded, consume the key
            wp_send_json_success([
                'message' => __('Campaign claimed! 5 points have been reserved. The owner will review your pitch.', 'consolety'),
                'claim' => $response['claim'] ?? null,
            ]);
        }

        // Handle specific errors
        $errorCode = $response['error']['code'] ?? ($response['error'] ?? 'UNKNOWN');
        $errorMessage = $response['error']['message'] ?? $response['error'] ?? __('Failed to claim campaign', 'consolety');

        // Non-retryable errors: consume the idempotency key so it doesn't linger
        $nonRetryable = ['ALREADY_CLAIMED', 'INSUFFICIENT_POINTS', 'CAMPAIGN_NOT_OPEN', 'SELF_CLAIM', 'CAMPAIGN_NOT_AVAILABLE'];
        if (is_string($errorCode) && in_array($errorCode, $nonRetryable, true)) {
            delete_transient($transientKey);
        }

        if (is_string($errorCode)) {
            switch ($errorCode) {
                case 'INSUFFICIENT_POINTS':
                    $errorMessage = __('Not enough points. You need 5 points to claim a campaign.', 'consolety');
                    break;
                case 'ALREADY_CLAIMED':
                    $errorMessage = __('You have already claimed this campaign.', 'consolety');
                    break;
                case 'CAMPAIGN_NOT_OPEN':
                    $errorMessage = __('This campaign is no longer available.', 'consolety');
                    break;
                case 'SELF_CLAIM':
                    $errorMessage = __('You cannot claim your own campaign.', 'consolety');
                    break;
            }
        }

        wp_send_json_error(['code' => $errorCode, 'message' => $errorMessage]);
    }

    /**
     * AJAX: Accept a claim (as campaign owner)
     *
     * Owner-side only: selects this publisher and rejects all others.
     * The publisher must then publish the article on their own site within 7 days.
     * No posts are created on the owner's site.
     */
    public function ajax_accept_claim() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);
        $claimId = absint($_POST['claim_id'] ?? 0);

        if (!$campaignId || !$claimId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid parameters']);
        }

        $response = $this->api->acceptClaim($campaignId, $claimId);

        if (!empty($response['success'])) {
            delete_transient('consolety_badge_counts');
            wp_send_json_success([
                'message' => __('Claim accepted! The publisher can now publish the article on their site.', 'consolety'),
            ]);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to accept claim'
        ]);
    }

    /**
     * AJAX: Publish a campaign article (as publisher/claimer)
     *
     * Called when the publisher clicks "Publish Article" after their claim was accepted.
     * Modeled after GuestPostsAjaxHandler::ajaxApprove() with rollback on Hub failure.
     */
    public function ajax_publish_campaign() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);
        $claimId = absint($_POST['claim_id'] ?? 0);
        $categoryId = absint($_POST['category_id'] ?? 0);

        if (!$campaignId || !$claimId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid parameters']);
        }

        // 1. Fetch campaign content from Hub
        $campaignResponse = $this->api->getCampaign($campaignId);

        if (empty($campaignResponse['success']) || empty($campaignResponse['campaign'])) {
            wp_send_json_error([
                'code' => $campaignResponse['error']['code'] ?? 'FETCH_FAILED',
                'message' => $campaignResponse['error']['message'] ?? 'Could not fetch campaign content'
            ]);
        }

        $campaign = $campaignResponse['campaign'];

        // Verify claim is accepted (publisher can only publish accepted claims)
        $myClaim = $campaign['my_claim'] ?? null;
        if (!$myClaim || $myClaim['status'] !== 'accepted') {
            wp_send_json_error([
                'code' => 'NOT_ACCEPTED',
                'message' => __('This claim has not been accepted yet.', 'consolety')
            ]);
        }

        // 2. Create WordPress post on THIS site (publisher's site)
        // Validate category exists on this site; fall back to default if invalid
        if ($categoryId && !term_exists($categoryId, 'category')) {
            $categoryId = 0;
        }
        $postCategories = $categoryId ? [$categoryId] : [get_option('default_category', 1)];

        // Clean content (wp_unslash removes residual slash layers)
        $content = wp_kses_post(wp_unslash($campaign['content'] ?? ''));
        $title = sanitize_text_field(wp_unslash($campaign['title'] ?? ''));

        $wpPostId = wp_insert_post([
            'post_title' => $title,
            'post_content' => $content,
            'post_status' => 'publish',
            'post_type' => 'post',
            'post_author' => get_current_user_id(),
            'post_category' => $postCategories,
            'meta_input' => [
                '_gp_campaign' => true,
                '_gp_campaign_id' => $campaignId,
            ],
        ], true);

        if (is_wp_error($wpPostId)) {
            wp_send_json_error([
                'code' => 'WP_ERROR',
                'message' => 'Failed to create WordPress post: ' . $wpPostId->get_error_message()
            ]);
        }

        // Store target URL as meta if present
        if (!empty($campaign['target_url'])) {
            update_post_meta($wpPostId, '_gp_target_url', esc_url_raw($campaign['target_url']));
        }

        // Set featured image if provided
        if (!empty($campaign['image_url'])) {
            $this->setFeaturedImageFromUrl($wpPostId, $campaign['image_url']);
        }

        $publishedUrl = get_permalink($wpPostId);

        // 3. Notify Hub that post was published (this triggers points finalization)
        $publishResponse = $this->api->markClaimPublished($campaignId, $claimId, $wpPostId, $publishedUrl);

        if (!empty($publishResponse['success'])) {
            delete_transient('consolety_badge_counts');
            wp_send_json_success([
                'message' => __('Article published successfully!', 'consolety'),
                'wp_post_id' => $wpPostId,
                'published_url' => $publishedUrl,
                'credits_earned' => $publishResponse['credits_earned'] ?? 0,
            ]);
        }

        // Rollback WP post if Hub notification failed
        // Clean up sideloaded featured image to prevent media orphans
        $thumbnailId = get_post_thumbnail_id($wpPostId);
        if ($thumbnailId) {
            wp_delete_attachment($thumbnailId, true);
        }
        wp_delete_post($wpPostId, true);
        wp_send_json_error([
            'code' => $publishResponse['error']['code'] ?? 'API_ERROR',
            'message' => $publishResponse['error']['message'] ?? 'Failed to confirm publication with Hub'
        ]);
    }

    /**
     * AJAX: Reject a claim (as campaign owner)
     */
    public function ajax_reject_claim() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);
        $claimId = absint($_POST['claim_id'] ?? 0);

        if (!$campaignId || !$claimId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid parameters']);
        }

        $response = $this->api->rejectClaim($campaignId, $claimId);

        if (!empty($response['success'])) {
            delete_transient('consolety_badge_counts');
            wp_send_json_success([
                'message' => __('Claim rejected. The publisher has been refunded their 5 points.', 'consolety'),
            ]);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to reject claim'
        ]);
    }

    /**
     * AJAX: Withdraw a campaign (as owner)
     */
    public function ajax_withdraw_campaign() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);

        if (!$campaignId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid campaign ID']);
        }

        $response = $this->api->withdrawCampaign($campaignId);

        if (!empty($response['success'])) {
            delete_transient('consolety_badge_counts');
            wp_send_json_success([
                'message' => __('Campaign withdrawn. Your 30 points have been refunded.', 'consolety'),
            ]);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to withdraw campaign'
        ]);
    }

    /**
     * Set featured image from URL with full SSRF protection.
     *
     * Parity with LinkMonitor::validateUrl() + checkLink():
     * - A + AAAA DNS resolution (IPv4 and IPv6)
     * - Manual redirect-hop validation (each hop re-checked)
     * - Private/reserved IP blocking on every resolved address
     */
    private function setFeaturedImageFromUrl($postId, $imageUrl) {
        // Validate the initial URL
        if (!$this->isPublicUrl($imageUrl)) {
            return;
        }

        // Follow redirects manually, validating each hop (max 5)
        $maxRedirects = 5;
        $currentUrl = $imageUrl;

        for ($hop = 0; $hop <= $maxRedirects; $hop++) {
            if (!$this->isPublicUrl($currentUrl)) {
                return; // Redirect target failed SSRF check
            }

            $response = wp_remote_head($currentUrl, [
                'timeout'     => 15,
                'redirection' => 0, // Do NOT follow redirects automatically
                'sslverify'   => true,
                'user-agent'  => 'Consolety/1.0',
            ]);

            if (is_wp_error($response)) {
                return; // Request failed
            }

            $httpCode = (int) wp_remote_retrieve_response_code($response);

            if (in_array($httpCode, [301, 302, 303, 307, 308], true)) {
                $location = wp_remote_retrieve_header($response, 'location');
                if (empty($location)) {
                    return; // Redirect without Location header
                }
                // Resolve relative redirects against current URL
                $currentUrl = \WP_Http::make_absolute_url($location, $currentUrl);
                continue;
            }

            // Not a redirect -- we have the final URL
            break;
        }

        // Final URL passed all SSRF checks -- safe to sideload
        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $attachmentId = media_sideload_image($currentUrl, $postId, '', 'id');

        if (!is_wp_error($attachmentId)) {
            set_post_thumbnail($postId, $attachmentId);
        }
    }

    /**
     * Validate that a URL points to a public (non-private, non-reserved) host.
     *
     * Checks both A (IPv4) and AAAA (IPv6) DNS records to prevent
     * SSRF via IPv6-only hosts or dual-stack DNS rebinding.
     *
     * @param string $url The URL to validate.
     * @return bool True if the URL is safe (public host), false otherwise.
     */
    private function isPublicUrl(string $url): bool {
        $parsed = wp_parse_url($url);
        if (!$parsed || empty($parsed['scheme']) || empty($parsed['host'])) {
            return false;
        }

        if (!in_array(strtolower($parsed['scheme']), ['http', 'https'], true)) {
            return false;
        }

        $host = $parsed['host'];

        // Reject IP-literal hostnames that are private/reserved
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return $this->isPublicIp($host);
        }

        // DNS resolution: check BOTH A (IPv4) and AAAA (IPv6) records
        $ips = gethostbynamel($host) ?: [];

        if (function_exists('dns_get_record')) {
            $dns6 = @dns_get_record($host, DNS_AAAA);
            if (!empty($dns6)) {
                $ips = array_merge($ips, array_column($dns6, 'ipv6'));
            }
        }

        if (empty($ips)) {
            return false; // DNS resolution failed
        }

        foreach ($ips as $ip) {
            if (!$this->isPublicIp($ip)) {
                return false; // Any private/reserved IP fails the whole check
            }
        }

        return true;
    }

    /**
     * Check if an IP address is public (not private or reserved).
     */
    private function isPublicIp(string $ip): bool {
        return filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        ) !== false;
    }

    /**
     * AJAX: Dismiss a campaign (hide from UI)
     */
    public function ajax_dismiss_campaign() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $campaignId = absint($_POST['campaign_id'] ?? 0);
        if (!$campaignId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid campaign ID']);
        }

        $response = $this->api->dismissCampaign($campaignId);

        if (!empty($response['success'])) {
            wp_send_json_success(['message' => __('Campaign dismissed.', 'consolety')]);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to dismiss campaign'
        ]);
    }

    /**
     * AJAX: Dismiss a campaign claim (hide from UI)
     */
    public function ajax_dismiss_claim() {
        check_ajax_referer('consolety_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['code' => 'UNAUTHORIZED', 'message' => 'Unauthorized']);
        }

        $claimId = absint($_POST['claim_id'] ?? 0);
        if (!$claimId) {
            wp_send_json_error(['code' => 'INVALID_INPUT', 'message' => 'Invalid claim ID']);
        }

        $response = $this->api->dismissClaim($claimId);

        if (!empty($response['success'])) {
            wp_send_json_success(['message' => __('Claim dismissed.', 'consolety')]);
        }

        wp_send_json_error([
            'code' => $response['error']['code'] ?? 'API_ERROR',
            'message' => $response['error']['message'] ?? 'Failed to dismiss claim'
        ]);
    }
}
