<?php
/**
 * Niwa AI – Frontend Chat Queue trait.
 *
 * Houses every method that drives the frontend chat queue, worker dispatching,
 * per-session lane locking and the generic named-option
 * lock primitives they all rely on.
 *
 * @package Niwa
 */

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

trait Niwa_AI_Frontend_Queue {

    private function find_or_enqueue_frontend_chat_job( $session, $message, $request_id = '', $request_context = array(), $include_full_history = null ) {
        $request_id = $this->sanitize_frontend_request_id( $request_id );
        $existing   = $this->find_pending_frontend_chat_job( $session, $message, $request_id );
        if ( ! empty( $existing ) ) {
            return $existing;
        }

        $lock_token = '';
        for ( $attempt = 0; $attempt < 6; $attempt++ ) {
            $lock_token = $this->acquire_named_option_lock( 'frontend_chat_queue', 3, 10 );
            if ( '' !== $lock_token ) {
                break;
            }
            usleep( 150000 );
        }
        if ( '' === $lock_token ) {
            return array(
                'id' => '',
            );
        }

        try {
            $queue  = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
            $queue  = is_array( $queue ) ? $queue : array();
			$active = array_filter(
				$queue,
				static function ( $queued_job ) {
					return is_array( $queued_job ) && in_array( (string) ( $queued_job['status'] ?? 'pending' ), array( 'pending', 'processing' ), true );
				}
			);
			$session_active = array_filter(
				$active,
				static function ( $queued_job ) use ( $session ) {
					return (string) ( $queued_job['session'] ?? '' ) === (string) $session;
				}
			);
			$queue_max         = max( 1, (int) apply_filters( 'niwa_ai_frontend_chat_queue_max', 100 ) );
			$session_queue_max = max( 1, (int) apply_filters( 'niwa_ai_frontend_chat_session_queue_max', 3 ) );
			if (
				count( $active ) >= $queue_max
				|| count( $session_active ) >= $session_queue_max
			) {
				return array( 'id' => '', 'error' => 'queue_full' );
			}
			$job_context = $this->normalize_frontend_chat_request_context( $request_context, $session );
			$job_context['queued_at_ms'] = microtime( true ) * 1000;
			$job_id = 'fchat_' . str_replace( '-', '', wp_generate_uuid4() );
            $job    = array(
                'id'         => $job_id,
                'session'    => $session,
                'message'    => $message,
                'request_id' => $request_id,
				'context'    => $job_context,
                'created_at' => time(),
                'status'     => 'pending',
                'attempts'   => 0,
                'include_full_history' => null === $include_full_history ? $this->should_return_frontend_full_history() : (bool) $include_full_history,
            );
            $queue[ $job_id ] = $job;
            update_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, $queue, false );

            return $job;
        } finally {
            $this->release_named_option_lock( 'frontend_chat_queue', $lock_token );
        }
    }

    private function find_pending_frontend_chat_job( $session, $message = '', $request_id = '' ) {
        $request_id = $this->sanitize_frontend_request_id( $request_id );
        $queue = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
        if ( ! is_array( $queue ) ) {
            return array();
        }

        foreach ( $queue as $job ) {
            if ( ! is_array( $job ) ) {
                continue;
            }
            if (
                '' !== $request_id
                && isset( $job['session'] )
                && (string) $job['session'] === (string) $session
                && (string) ( $job['request_id'] ?? '' ) === $request_id
                && in_array( (string) ( $job['status'] ?? 'pending' ), array( 'pending', 'processing' ), true )
            ) {
                return $job;
            }
            if (
                isset( $job['session'] )
                && (string) $job['session'] === (string) $session
                && (string) ( $job['message'] ?? '' ) === (string) $message
                && in_array( (string) ( $job['status'] ?? 'pending' ), array( 'pending', 'processing' ), true )
            ) {
                return $job;
            }
        }

        return array();
    }

	private function get_frontend_chat_job_status( $job_id, $session = '' ) {
        $job_id = trim( (string) $job_id );
        if ( '' === $job_id ) {
            return array( 'status' => 'missing' );
        }

        $result = Niwa_AI_Durable_Cache::get( 'niwa_ai_frontend_chat_job_' . md5( $job_id ) );
		if (
			is_array( $result )
			&& ! empty( $result['status'] )
			&& ( '' === (string) $session || hash_equals( (string) ( $result['session'] ?? '' ), (string) $session ) )
		) {
            return $result;
        }

        $queue = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
		if (
			is_array( $queue )
			&& ! empty( $queue[ $job_id ] )
			&& ( '' === (string) $session || hash_equals( (string) ( $queue[ $job_id ]['session'] ?? '' ), (string) $session ) )
		) {
            return array(
                'status' => isset( $queue[ $job_id ]['status'] ) ? (string) $queue[ $job_id ]['status'] : 'pending',
            );
        }

        return array(
            'status' => 'missing',
        );
    }

    public function process_frontend_chat_queue( $limit = 2 ) {
        $lock_token = $this->acquire_named_option_lock( 'frontend_chat_queue_runner', 2, 90 );
        if ( '' === $lock_token ) {
            return array(
                'success' => true,
                'processed' => 0,
                'note' => 'runner_locked',
            );
        }

        $processed = 0;

        try {
            while ( $processed < max( 1, intval( $limit ) ) ) {
                $job = $this->claim_next_frontend_chat_job();
                if ( empty( $job['id'] ) ) {
                    break;
                }

                $session = isset( $job['session'] ) ? (string) $job['session'] : '';
                $lane_ok = $this->acquire_frontend_chat_lane( $session, 75 );
                if ( ! $lane_ok ) {
                    $this->requeue_frontend_chat_job( $job['id'], 'pending' );
                    break;
                }

                try {
                    $history = $this->conversation_store ? $this->conversation_store->get( $session ) : array();
                    $history = $this->ensure_frontend_opening_context_in_history( $session, $history );
                    $include_full_history = ! empty( $job['include_full_history'] );
                    $request_id           = isset( $job['request_id'] ) ? $this->sanitize_frontend_request_id( $job['request_id'] ) : '';
                    $request_context      = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
                    $this->set_frontend_chat_request_status(
                        $session,
                        $request_id,
                        'processing',
                        array(
                            'queue_token' => (string) $job['id'],
                        )
                    );
                    $run = $this->run_frontend_chat_request( isset( $job['message'] ) ? (string) $job['message'] : '', $session, $history, $include_full_history, $request_context );

                    if ( empty( $run['success'] ) ) {
                        $error = isset( $run['error'] ) ? (string) $run['error'] : 'Queued chat failed.';
						$retryable = ! empty( $run['retryable'] );
                        Niwa_AI_Durable_Cache::set(
                            'niwa_ai_frontend_chat_job_' . md5( $job['id'] ),
                            array(
                                'status' => 'failed',
                                'error'  => $error,
								'retryable' => $retryable,
								'session' => $session,
                            ),
                            15 * MINUTE_IN_SECONDS
                        );
						$this->set_frontend_chat_request_status(
							$session,
							$request_id,
							'failed',
							array( 'error' => $error, 'retryable' => $retryable )
						);
                    } else {
                        $payload = isset( $run['payload'] ) && is_array( $run['payload'] ) ? $run['payload'] : array();
                        Niwa_AI_Durable_Cache::set(
                            'niwa_ai_frontend_chat_job_' . md5( $job['id'] ),
                            array(
                                'status'  => 'done',
                                'payload' => $payload,
								'session' => $session,
                            ),
                            15 * MINUTE_IN_SECONDS
                        );
                        $this->set_frontend_chat_request_status( $session, $request_id, 'done', array( 'payload' => $payload ) );
                    }

                    $this->remove_frontend_chat_job( $job['id'] );
                    $processed++;
                } finally {
                    $this->release_frontend_chat_lane( $session );
                }
            }

            return array(
                'success'   => true,
                'processed' => $processed,
            );
        } finally {
            $this->release_named_option_lock( 'frontend_chat_queue_runner', $lock_token );
        }
    }

    public function handle_frontend_chat_worker() {
        ignore_user_abort( true );
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( 60 );
        }

        if ( ! $this->verify_frontend_worker_signature() ) {
            status_header( 403 );
            echo 'forbidden';
            exit;
        }

        $result = $this->process_frontend_chat_queue( 2 );
        if ( $this->has_pending_frontend_chat_jobs() ) {
            $this->dispatch_frontend_chat_worker( 'chain' );
        }

        status_header( empty( $result['success'] ) ? 500 : 200 );
        echo empty( $result['success'] ) ? 'error' : 'ok';
        exit;
    }

    private function dispatch_frontend_chat_worker( $reason = 'queue' ) {
        $secret = $this->get_frontend_worker_secret();
        $ts     = time();
        $sig    = hash_hmac( 'sha256', 'frontend_chat_worker|' . $ts, $secret );

        wp_remote_post(
            add_query_arg(
                array(
                    'action' => 'niwa_ai_frontend_chat_worker',
                    'reason' => sanitize_key( $reason ),
                ),
                admin_url( 'admin-post.php' )
            ),
            array(
                'timeout'   => 1,
                'blocking'  => false,
				'sslverify' => true,
                'body'      => array(
                    'niwa_ts'  => $ts,
                    'niwa_sig' => $sig,
                ),
            )
        );
    }

    private function verify_frontend_worker_signature() {
        $ts  = isset( $_POST['niwa_ts'] ) ? intval( $_POST['niwa_ts'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification
        $sig = isset( $_POST['niwa_sig'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_sig'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
        if ( 0 === $ts || '' === $sig || abs( time() - $ts ) > 120 ) {
            return false;
        }

        $secret   = $this->get_frontend_worker_secret();
        $expected = hash_hmac( 'sha256', 'frontend_chat_worker|' . $ts, $secret );
        return hash_equals( $expected, $sig );
    }

    private function get_frontend_worker_secret() {
        $secret = get_option( Niwa_AI_Constants::OPT_FRONTEND_WORKER_SECRET, '' );
        if ( is_string( $secret ) && '' !== $secret ) {
            return $secret;
        }

        $secret = wp_generate_password( 48, false, false );
        update_option( Niwa_AI_Constants::OPT_FRONTEND_WORKER_SECRET, $secret, false );
        return $secret;
    }

    private function claim_next_frontend_chat_job() {
        $lock_token = $this->acquire_named_option_lock( 'frontend_chat_queue', 2, 10 );
        if ( '' === $lock_token ) {
            return array();
        }

        try {
            $queue = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
            $queue = is_array( $queue ) ? $queue : array();
            foreach ( $queue as $job_id => $job ) {
                if ( ! is_array( $job ) ) {
                    unset( $queue[ $job_id ] );
                    continue;
                }
                $status = isset( $job['status'] ) ? (string) $job['status'] : 'pending';
                if ( 'processing' === $status ) {
                    $processing_at = isset( $job['processing_at'] ) ? intval( $job['processing_at'] ) : 0;
                    if ( $processing_at > 0 && $processing_at + 120 < time() && intval( $job['attempts'] ?? 0 ) < 3 ) {
                        $status        = 'pending';
                        $job['status'] = 'pending';
                    }
                }
                if ( 'pending' !== $status ) {
                    continue;
                }
                if ( intval( $job['attempts'] ?? 0 ) >= 3 ) {
                    $failed_session    = isset( $job['session'] ) ? (string) $job['session'] : '';
                    $failed_request_id = isset( $job['request_id'] ) ? $this->sanitize_frontend_request_id( $job['request_id'] ) : '';
                    Niwa_AI_Durable_Cache::set(
                        'niwa_ai_frontend_chat_job_' . md5( $job_id ),
                        array(
                            'status' => 'failed',
                            'error'  => 'Queued chat failed after repeated worker attempts.',
							'session' => $failed_session,
                        ),
                        15 * MINUTE_IN_SECONDS
                    );
                    $this->set_frontend_chat_request_status(
                        $failed_session,
                        $failed_request_id,
                        'failed',
                        array(
                            'error' => 'Queued chat failed after repeated worker attempts.',
                        )
                    );
                    unset( $queue[ $job_id ] );
                    continue;
                }
                $job['status']      = 'processing';
                $job['processing_at'] = time();
                $job['attempts']    = intval( isset( $job['attempts'] ) ? $job['attempts'] : 0 ) + 1;
                $queue[ $job_id ]   = $job;
                update_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, $queue, false );
                return $job;
            }

            update_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, $queue, false );
            return array();
        } finally {
            $this->release_named_option_lock( 'frontend_chat_queue', $lock_token );
        }
    }

    private function requeue_frontend_chat_job( $job_id, $status = 'pending' ) {
        $lock_token = $this->acquire_named_option_lock( 'frontend_chat_queue', 2, 10 );
        if ( '' === $lock_token ) {
            return;
        }

        try {
            $queue = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
            $queue = is_array( $queue ) ? $queue : array();
            if ( ! empty( $queue[ $job_id ] ) && is_array( $queue[ $job_id ] ) ) {
                $queue[ $job_id ]['status'] = $status;
                update_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, $queue, false );
            }
        } finally {
            $this->release_named_option_lock( 'frontend_chat_queue', $lock_token );
        }
    }

    private function remove_frontend_chat_job( $job_id ) {
        $lock_token = $this->acquire_named_option_lock( 'frontend_chat_queue', 2, 10 );
        if ( '' === $lock_token ) {
            return;
        }

        try {
            $queue = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
            $queue = is_array( $queue ) ? $queue : array();
            if ( isset( $queue[ $job_id ] ) ) {
                unset( $queue[ $job_id ] );
                update_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, $queue, false );
            }
        } finally {
            $this->release_named_option_lock( 'frontend_chat_queue', $lock_token );
        }
    }

    private function has_pending_frontend_chat_jobs() {
        $queue = get_option( Niwa_AI_Constants::OPT_FRONTEND_CHAT_QUEUE, array() );
        if ( ! is_array( $queue ) ) {
            return false;
        }

        foreach ( $queue as $job ) {
            if ( ! is_array( $job ) ) {
                continue;
            }
            if ( in_array( (string) ( $job['status'] ?? 'pending' ), array( 'pending', 'processing' ), true ) ) {
                return true;
            }
        }

        return false;
    }

    private function acquire_named_option_lock( $name, $wait_seconds = 2, $ttl = 10 ) {
        global $wpdb;

        $option_name = 'niwa_ai_named_lock_' . sanitize_key( $name );
        $deadline    = microtime( true ) + max( 1, intval( $wait_seconds ) );
        $ttl         = max( 5, intval( $ttl ) );

        do {
            $token = ( time() + $ttl ) . ':' . wp_generate_password( 12, false, false );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            $inserted = $wpdb->query(
                $wpdb->prepare(
                    "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')",
                    $option_name,
                    $token
                )
            );
            wp_cache_delete( $option_name, 'options' );
            wp_cache_delete( 'notoptions', 'options' );
            if ( $inserted ) {
                return $token;
            }

            $existing = get_option( $option_name, '' );
            $parts    = is_string( $existing ) ? explode( ':', $existing, 2 ) : array();
            $expires  = isset( $parts[0] ) ? intval( $parts[0] ) : 0;
            if ( $expires <= time() ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                $claimed = $wpdb->query(
                    $wpdb->prepare(
                        "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
                        $token,
                        $option_name,
                        (string) $existing
                    )
                );
                wp_cache_delete( $option_name, 'options' );
                wp_cache_delete( 'notoptions', 'options' );
                if ( $claimed ) {
                    return $token;
                }
            }

            usleep( 150000 );
        } while ( microtime( true ) < $deadline );

        return '';
    }

    private function release_named_option_lock( $name, $token ) {
        global $wpdb;

        $option_name = 'niwa_ai_named_lock_' . sanitize_key( $name );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
                $option_name,
                (string) $token
            )
        );
        wp_cache_delete( $option_name, 'options' );
        wp_cache_delete( 'notoptions', 'options' );
    }

    private function acquire_frontend_chat_lane( $session_id, $ttl = 45 ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return true;
        }

        $option_key = Niwa_AI_Constants::OPT_FRONTEND_LANE_PREFIX . md5( $session_id );
        $now        = time();
        $payload    = array(
            'session_id'  => $session_id,
            'acquired_at' => $now,
            'expires_at'  => $now + max( 15, intval( $ttl ) ),
        );

        if ( add_option( $option_key, $payload, '', false ) ) {
            return true;
        }

        $existing = get_option( $option_key, array() );
        $expires  = isset( $existing['expires_at'] ) ? intval( $existing['expires_at'] ) : 0;
        if ( $expires > $now ) {
            return false;
        }

        return update_option( $option_key, $payload, false );
    }

    private function release_frontend_chat_lane( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return;
        }

        delete_option( Niwa_AI_Constants::OPT_FRONTEND_LANE_PREFIX . md5( $session_id ) );
    }
}
