<?php
defined( 'ABSPATH' ) || exit;

/**
 * Async job engine.
 * Stores job state in wp_options, fires via WP-Cron.
 * Decouples the heavy AI API call from the user's browser request —
 * solves shared hosting PHP timeout limits (Bravehost, SiteGround, etc.).
 *
 * Job lifecycle: queued → processing → done | failed
 */
class PIPEBU_Job {

    // -------------------------------------------------------------------------
    // Queue a new job, schedule WP-Cron to fire it immediately.
    // Returns job_id string.
    // -------------------------------------------------------------------------

    public static function queue( array $attachment_ids ): string {
        $job_id = 'pipebu_job_' . wp_generate_password( 12, false );

        $job = [
            'id'             => $job_id,
            'status'         => 'queued',
            'attachment_ids' => array_map( 'absint', $attachment_ids ),
            'result'         => null,
            'error'          => null,
            'created'        => time(),
            'updated'        => time(),
        ];

        update_option( $job_id, $job, false );

        // Schedule immediate single-fire cron event.
        wp_schedule_single_event( time(), 'pipebu_process_job', [ $job_id ] );

        // Spawn cron now so we don't wait for next real traffic hit.
        self::spawn_cron();

        return $job_id;
    }

    // -------------------------------------------------------------------------
    // WP-Cron callback — runs the actual AI generation.
    // -------------------------------------------------------------------------

    public static function process( string $job_id ): void {
        $job = get_option( $job_id );
        if ( ! $job || $job['status'] !== 'queued' ) {
            return;
        }

        // Mark as processing.
        $job['status']  = 'processing';
        $job['updated'] = time();
        update_option( $job_id, $job, false );

        // Fetch tone reference content if a post ID was provided.
        $tone_reference = '';
        if ( ! empty( $job['tone_reference_id'] ) ) {
            $ref_post = get_post( absint( $job['tone_reference_id'] ) );
            if ( $ref_post && ! empty( $ref_post->post_content ) ) {
                $raw   = wp_strip_all_tags( $ref_post->post_content );
                $words = preg_split( '/\s+/', trim( $raw ) );
                $tone_reference = implode( ' ', array_slice( $words, 0, 600 ) );
            }
        }

        // Run generation — pass all options stored with job.
        try {
            $result = PIPEBU_Generator::run(
                $job['attachment_ids'],
                $job['depth']            ?? 'standard',
                $job['tone']             ?? 'professional',
                $job['custom_style']     ?? '',
                (bool) ( $job['guide_mode']    ?? false ),
                $job['template']         ?? 'none',
                $tone_reference,
                (bool) ( $job['has_colors']    ?? false ),
                $job['output_language']  ?? ''
            );
        } catch ( \Throwable $e ) {
            $result = new \WP_Error( 'pipebu_exception', $e->getMessage() );
        }

        if ( is_wp_error( $result ) ) {
            $job['status'] = 'failed';
            $job['error']  = $result->get_error_message();
            $err_data      = $result->get_error_data();
            $err_status    = is_array( $err_data ) ? (int) ( $err_data['status'] ?? 0 ) : 0;
            $err_detail    = is_array( $err_data ) ? (string) ( $err_data['detail'] ?? '' ) : '';
            PIPEBU_Quota::report_failure( self::classify_error( $result ), $err_status, $err_detail );
        } else {
            $job['status'] = 'done';
            $job['result'] = $result;
        }

        $job['updated'] = time();
        update_option( $job_id, $job, false );

        // Auto-cleanup after 1 hour.
        wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'pipebu_cleanup_job', [ $job_id ] );
    }

    // -------------------------------------------------------------------------
    // Poll job status — called by JS every 3 seconds.
    // -------------------------------------------------------------------------

    public static function poll( string $job_id ): array {
        $job = get_option( $job_id );
        if ( ! $job ) {
            return [ 'status' => 'not_found' ];
        }
        return [
            'status' => $job['status'],
            'result' => $job['result'] ?? null,
            'error'  => $job['error'] ?? null,
            'age'    => time() - $job['created'],
        ];
    }

    // -------------------------------------------------------------------------
    // Classify a WP_Error into a telemetry failure type string.
    // -------------------------------------------------------------------------

    private static function classify_error( \WP_Error $err ): string {
        $code   = $err->get_error_code();
        $msg    = strtolower( $err->get_error_message() );
        $data   = $err->get_error_data();
        $status = is_array( $data ) ? (int) ( $data['status'] ?? 0 ) : 0;

        switch ( $code ) {
            case 'pb_no_key':
                return 'no_key';
            case 'pb_missing':
            case 'pb_file':
                return 'missing_file';
            case 'pipebu_parse':
                return 'parse_fail';
            case 'pipebu_exception':
                return 'exception';
            case 'pipebu_api_error':
                // Numeric status from the API response is the reliable signal —
                // string-matching the message text is a fallback for the rare
                // case a WP_Error reaches here without status data attached.
                if ( $status === 429 )                     return 'rate_limit';
                if ( $status === 401 || $status === 403 )  return 'auth';
                if ( $status >= 500 )                      return 'provider';
                if ( str_contains( $msg, '429' ) )  return 'rate_limit';
                if ( str_contains( $msg, '401' ) )  return 'auth';
                if ( preg_match( '/\b5\d{2}\b/', $msg ) ) return 'provider';
                return 'other';
        }

        // WP HTTP API transport errors (is_wp_error on the response itself).
        if ( str_contains( $msg, 'timed out' ) || str_contains( $msg, 'curl error 28' ) ) {
            return 'timeout';
        }
        if ( str_contains( $msg, 'curl error' ) || str_contains( $msg, 'could not resolve' )
             || str_contains( $msg, 'connection refused' ) || str_contains( $msg, 'ssl' ) ) {
            return 'network';
        }

        return 'other';
    }

    // -------------------------------------------------------------------------
    // Cleanup
    // -------------------------------------------------------------------------

    public static function cleanup( string $job_id ): void {
        delete_option( $job_id );
    }

    // -------------------------------------------------------------------------
    // Spawn cron without waiting for next page load.
    // -------------------------------------------------------------------------

    private static function spawn_cron(): void {
        $cron_url = add_query_arg( 'doing_wp_cron', '', site_url( 'wp-cron.php' ) );
        wp_remote_post( $cron_url, [
            'timeout'   => 0.01,
            'blocking'  => false,
            'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
        ] );
    }
}

// Cron cleanup hook.
add_action( 'pipebu_cleanup_job', [ 'PIPEBU_Job', 'cleanup' ] );
