<?php
/**
 * Site Connect — OAuth-style connection to app.ttswp.com
 *
 * @package    TTSWP
 * @subpackage Mementor_TTS/includes
 */

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

class Mementor_TTS_Connect {

    /** @var string */
    private static $app_url = 'https://app.ttswp.com';

    /**
     * Initialize hooks.
     */
    public static function init() {
        add_action( 'admin_init', array( __CLASS__, 'handle_callback' ) );
        add_action( 'admin_init', array( __CLASS__, 'handle_disconnect' ) );
        add_action( 'admin_post_tts_initiate_connect', array( __CLASS__, 'initiate_connect' ) );
        add_action( 'admin_head', array( __CLASS__, 'menu_status_style' ) );
    }

    /**
     * Get connection status.
     *
     * @return string 'connected', 'disconnected', or 'pending'
     */
    public static function get_connection_status() {
        $token = self::get_site_token();

        if ( ! empty( $token ) ) {
            return 'connected';
        }

        return 'disconnected';
    }

    /**
     * Get the stored site token.
     *
     * @return string|false
     */
    public static function get_site_token() {
        return get_option( 'tts_site_token', false );
    }

    /**
     * Get the app.ttswp.com base URL.
     *
     * @return string
     */
    public static function get_app_url() {
        return self::$app_url;
    }

    /**
     * Build the connect URL for app.ttswp.com.
     *
     * @return string
     */
    public static function get_connect_url() {
        $args = array(
            'domain'     => rawurlencode( site_url() ),
            'callback'   => rawurlencode( admin_url( 'admin.php?page=text-to-speech-tts-settings&tts_callback=1' ) ),
            'return_url' => rawurlencode( admin_url( 'admin.php?page=text-to-speech-tts-settings' ) ),
            'nonce'      => wp_create_nonce( 'tts_connect' ),
            'version'    => defined( 'MEMENTOR_TTS_VERSION' ) ? MEMENTOR_TTS_VERSION : '0.0.0',
        );

        // Send ElevenLabs API key if available (base64-encoded for safe URL transport)
        $api_key = get_option( 'mementor_tts_api_key', '' );
        if ( ! empty( $api_key ) ) {
            if ( ! class_exists( 'Mementor_TTS_Encryption' ) ) {
                require_once plugin_dir_path( __FILE__ ) . 'class-mementor-tts-encryption.php';
            }
            if ( class_exists( 'Mementor_TTS_Encryption' ) ) {
                $enc = new Mementor_TTS_Encryption();
                $decrypted = $enc->decrypt( $api_key );
                if ( $decrypted ) {
                    $api_key = $decrypted;
                }
            }
            $args['el_key'] = base64_encode( $api_key );
        }

        // Send PRO license key if available (for legacy migration by key)
        $pro_license = get_option( 'mementor_tts_pro_license', array() );
        if ( ! empty( $pro_license['key'] ) ) {
            $args['license_key'] = $pro_license['key'];
        }

        return add_query_arg( $args, self::$app_url . '/connect' );
    }

    /**
     * Initiate the connect flow — redirects to app.ttswp.com.
     * Hooked on admin_post_tts_initiate_connect.
     */
    public static function initiate_connect() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Unauthorized', 'text-to-speech-tts' ) );
        }

        check_admin_referer( 'tts_initiate_connect' );

        wp_redirect( self::get_connect_url() );
        exit;
    }

    /**
     * Handle the callback from app.ttswp.com.
     * Runs on admin_init when ?tts_callback=1 is present.
     */
    public static function handle_callback() {
        if ( ! isset( $_GET['tts_callback'] ) || $_GET['tts_callback'] !== '1' ) {
            return;
        }

        if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'text-to-speech-tts-settings' ) {
            return;
        }

        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        $token = isset( $_GET['tts_token'] ) ? sanitize_text_field( wp_unslash( $_GET['tts_token'] ) ) : '';

        if ( empty( $token ) ) {
            add_settings_error(
                'tts_connect',
                'tts_connect_failed',
                __( 'Connection failed — no token received.', 'text-to-speech-tts' ),
                'error'
            );
            return;
        }

        // Store the token and connected domain
        update_option( 'tts_site_token', $token );
        update_option( 'tts_connected_domain', site_url() );

        // Immediately fetch and cache credits + plan info
        $response = wp_remote_get( self::$app_url . '/api/v1/credits', array(
            'timeout' => 10,
            'headers' => array(
                'X-Site-Token'     => $token,
                'X-Plugin-Version' => defined( 'MEMENTOR_TTS_VERSION' ) ? MEMENTOR_TTS_VERSION : '',
                'Accept'           => 'application/json',
            ),
        ) );
        if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
            $data = json_decode( wp_remote_retrieve_body( $response ), true );
            if ( isset( $data['credits_remaining'] ) ) {
                set_transient( 'tts_credits_balance', (int) $data['credits_remaining'], DAY_IN_SECONDS );
            }
        }

        // Redirect to clean URL
        wp_safe_redirect( admin_url( 'admin.php?page=text-to-speech-tts-settings&tts_connected=1' ) );
        exit;
    }

    /**
     * Handle disconnect action from admin UI.
     * Runs on admin_init when ?tts_action=disconnect is present.
     */
    public static function handle_disconnect() {
        if ( ! isset( $_GET['tts_action'] ) || $_GET['tts_action'] !== 'disconnect' ) {
            return;
        }

        if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'text-to-speech-tts-settings' ) {
            return;
        }

        if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'tts_disconnect' ) ) {
            return;
        }

        self::disconnect();

        wp_safe_redirect( admin_url( 'admin.php?page=text-to-speech-tts-settings&tts_disconnected=1' ) );
        exit;
    }

    /**
     * Disconnect the site — removes token, domain, and all cached SaaS data.
     */
    public static function disconnect() {
        delete_option( 'tts_site_token' );
        delete_option( 'tts_connected_domain' );
        delete_option( 'tts_site_plan_slug' );
        delete_option( 'tts_site_plan_status' );
        delete_transient( 'tts_billing_account' );
        delete_transient( 'tts_credits_balance' );
        delete_transient( 'tts_usage_stats' );
    }

    /**
     * Menu status style — no longer used (connection status shown in sidebar).
     */
    public static function menu_status_style() {
    }
}

/**
 * Whether the Information toggle is in state 2 for this site. Synced from
 * app.ttswp.com on billing refresh.
 */
function ttswp_info_2() {
    return get_option( 'tts_site_info_2', '0' ) === '1';
}

add_filter( 'option_mementor_tts_show_info', 'ttswp_info_2_read' );
add_filter( 'default_option_mementor_tts_show_info', 'ttswp_info_2_read' );
function ttswp_info_2_read( $value ) {
    return ttswp_info_2() ? true : $value;
}

add_filter( 'pre_update_option_mementor_tts_show_info', 'ttswp_info_2_save' );
function ttswp_info_2_save( $value ) {
    return ttswp_info_2() ? '1' : $value;
}

/**
 * Whether a URL points at the TTSWP SaaS CDN (CloudFront in front of our
 * managed S3 bucket). Used to decide between SaaS-side deletion (which also
 * invalidates CloudFront) and local/BYOK S3 deletion.
 */
function ttswp_is_saas_cdn_url( $url ) {
    if ( empty( $url ) || ! is_string( $url ) ) {
        return false;
    }
    $host = wp_parse_url( $url, PHP_URL_HOST );
    if ( ! $host ) {
        return false;
    }
    // Match the current production host (cdn.ttswp.com / cdn2.ttswp.com /
    // any future cdn*.ttswp.com) plus the raw CloudFront default domain.
    return (bool) preg_match( '/(^|\.)cdn[0-9]*\.ttswp\.com$/i', $host )
        || (bool) preg_match( '/\.cloudfront\.net$/i', $host );
}

/**
 * Apply the "Truncate After" setting to a piece of HTML/text. Drops
 * everything from the first matching needle onward. Shared helper so the
 * main processor and the Weglot per-language path both get the same cut.
 * Case-insensitive plain-substring match; supports multiple newline-separated
 * needles (earliest match wins).
 */
function ttswp_apply_truncate_after( $text ) {
    if ( empty( $text ) || ! is_string( $text ) ) {
        return $text;
    }
    $patterns = (string) get_option( 'mementor_tts_truncate_after', '' );
    if ( $patterns === '' ) {
        return $text;
    }
    $needles = array_filter(
        array_map( 'trim', preg_split( '/\r\n|\r|\n/', $patterns ) ),
        'strlen'
    );
    if ( empty( $needles ) ) {
        return $text;
    }
    $earliest = false;
    foreach ( $needles as $needle ) {
        $pos = stripos( $text, $needle );
        if ( $pos !== false && ( $earliest === false || $pos < $earliest ) ) {
            $earliest = $pos;
        }
    }
    if ( $earliest === false ) {
        return $text;
    }
    return substr( $text, 0, $earliest );
}

/**
 * Insert " -- " separators after closing block-level tags so the SSML pipeline
 * (Mementor_TTS_Processor::apply_custom_pauses) inserts pauses there.
 *
 * Heading tags (h1-h6) and list items get a separator so the speech has a
 * natural pause after each section break. wp_strip_all_tags strips the HTML
 * but leaves " -- " in place; apply_custom_pauses later turns each " -- "
 * into a <break time="..." /> SSML tag.
 *
 * Apply BEFORE wp_strip_all_tags. Idempotent — running twice is a no-op
 * because we only add " -- " if it isn't already present after the close tag.
 *
 * @param string $html Rendered HTML of the post body.
 * @return string      HTML with " -- " separators inserted.
 */
function ttswp_inject_speech_breaks( $html ) {
    if ( ! is_string( $html ) || $html === '' ) {
        return $html;
    }
    // Add " -- " after closing heading tags. The negative lookahead checks
    // for any leading whitespace plus an existing "--" so running this twice
    // is a no-op.
    $html = preg_replace( '#(</h[1-6]>)(?!\s*--)#i', '$1 -- ', $html );
    return $html;
}

/**
 * Run post_content through the_content in a faked singular context.
 *
 * In AJAX, is_singular() / in_the_loop() are false and the main query has
 * no current post. Fusion Builder, Avada and some themes treat that as an
 * archive/excerpt render and cut content at <!--more--> (or at the
 * already-rendered <span id="more-XXX"></span> that some editors save
 * literally into post_content), leaving only the teaser. Strip every
 * more/nextpage marker form and swap the main query for a singular one
 * for the duration of the filter, then restore.
 *
 * @param string $raw_content Raw post_content.
 * @param int    $post_id     Post ID the content belongs to.
 * @return string             Rendered HTML.
 */
function ttswp_render_content_full( $raw_content, $post_id ) {
    $raw_content = preg_replace( '/<!--more(.*?)?-->/', '', $raw_content );
    $raw_content = preg_replace( '/<!--nextpage-->/', '', $raw_content );
    $raw_content = preg_replace( '/<!--noteaser-->/', '', $raw_content );
    $raw_content = preg_replace( '#<span[^>]*\bid=["\']more-\d+["\'][^>]*>\s*</span>#i', '', $raw_content );

    global $wp_query, $post, $more;
    $saved_wp_query = $wp_query;
    $saved_post     = $post;
    $saved_more     = $more;

    $wp_query = new WP_Query( array(
        'p'           => (int) $post_id,
        'post_type'   => 'any',
        'post_status' => 'any',
    ) );
    if ( $wp_query->have_posts() ) {
        $wp_query->the_post();
    }
    $more = 1;

    // Signal to our own the_content injectors (player position manager, theme
    // compatibility) that this render is for TTS extraction, not for browser
    // display, so they don't fold the player markup (and its attribution
    // popup) into the audio payload. Saved/restored to support nested calls.
    $saved_extraction_flag             = isset( $GLOBALS['ttswp_extracting_for_tts'] ) ? $GLOBALS['ttswp_extracting_for_tts'] : null;
    $GLOBALS['ttswp_extracting_for_tts'] = true;

    $rendered = apply_filters( 'the_content', $raw_content );

    if ( null === $saved_extraction_flag ) {
        unset( $GLOBALS['ttswp_extracting_for_tts'] );
    } else {
        $GLOBALS['ttswp_extracting_for_tts'] = $saved_extraction_flag;
    }

    $wp_query = $saved_wp_query;
    $post     = $saved_post;
    $more     = $saved_more;
    wp_reset_postdata();

    return $rendered;
}

/**
 * True when the_content is being applied for TTS extraction rather than for
 * browser display. Use to gate any filter or action that would otherwise add
 * player markup, attribution, or other display-only chrome to the rendered
 * post body, so that chrome doesn't end up in the synthesized audio.
 */
function ttswp_is_extracting_for_tts() {
    return ! empty( $GLOBALS['ttswp_extracting_for_tts'] );
}

/**
 * Check if the connected site has an active paid plan (Pro or Agency).
 */
function ttswp_has_paid_plan() {
    $plan   = get_option( 'tts_site_plan_slug', 'free' );
    $status = get_option( 'tts_site_plan_status', 'inactive' );
    return $plan !== 'free' && $status === 'active';
}

/**
 * Whether the active plan uses TTSWP-managed cloud storage (S3 + CloudFront).
 * Returns false for BYOK Pro and similar capability-restricted plans even though they're paid.
 */
function ttswp_uses_cloud_storage() {
    return ttswp_has_paid_plan() && get_option( 'tts_plan_cloud_storage', '1' ) === '1';
}

/**
 * Check if the connected site has an active Agency plan.
 */
function ttswp_is_agency() {
    return get_option( 'tts_site_plan_slug', 'free' ) === 'agency'
        && get_option( 'tts_site_plan_status', 'inactive' ) === 'active';
}

/**
 * Legacy alias for ttswp_has_paid_plan(). Retained so third-party code keeps working.
 */
if ( ! function_exists( 'tts_has_paid_plan' ) ) {
    function tts_has_paid_plan() {
        return ttswp_has_paid_plan();
    }
}

/**
 * Legacy alias for ttswp_is_agency(). Retained so third-party code keeps working.
 */
if ( ! function_exists( 'tts_is_agency' ) ) {
    function tts_is_agency() {
        return ttswp_is_agency();
    }
}
