<?php

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

class Niwa_AI_Admin_Autonomy_Runner_Dispatcher {

    /** @var Niwa_AI_Admin_Autonomy_Telegram|null */
    private $telegram;

    /** @var array<string, callable> */
    private $callbacks;

    public function __construct( $telegram = null, $callbacks = array() ) {
        $this->telegram  = $telegram;
        $this->callbacks = is_array( $callbacks ) ? $callbacks : array();
    }

    public function dispatch( $reason = 'boot', $active_ttl = 30 ) {
        $reason_key = sanitize_key( (string) $reason );
		if ( ! $this->telegram ) {
			return array(
				'success' => false,
				'error'   => 'runner_state_storage_unavailable',
            );
		}

        $secret = $this->telegram->ensure_runner_secret();
        $state  = $this->telegram->get_runner_state();

        if ( 'force' !== $reason_key && $this->has_active_actor_messages() ) {
            $state['wake_pending']      = 1;
            $state['wake_reason']       = $reason_key;
            $state['wake_requested_at'] = time();
            $this->telegram->update_runner_state( $state );

            $this->log(
                'loopback_runner_dispatch_skipped',
                array(
                    'reason'       => $reason_key,
                    'error'        => 'actor_already_active',
                    'wake_pending' => true,
                )
            );

            return array(
                'success'      => false,
                'error'        => 'actor_already_active',
                'wake_pending' => true,
            );
        }

        if ( 'chain' === $reason_key && intval( isset( $state['last_spawned_at'] ) ? $state['last_spawned_at'] : 0 ) > ( time() - 3 ) ) {
            $this->log(
                'loopback_runner_dispatch_skipped',
                array(
                    'reason' => $reason_key,
                    'error'  => 'chain_cooldown',
                )
            );

            return array(
                'success' => false,
                'error'   => 'chain_cooldown',
            );
        }

        if ( intval( isset( $state['active_until'] ) ? $state['active_until'] : 0 ) > time() ) {
            $state = $this->clear_stale_buffer_runner_state( $state, $reason_key );
        }

        if ( intval( isset( $state['active_until'] ) ? $state['active_until'] : 0 ) > time() ) {
            $state['wake_pending']      = 1;
            $state['wake_reason']       = $reason_key;
            $state['wake_requested_at'] = time();
            $this->telegram->update_runner_state( $state );

            $this->log(
                'loopback_runner_dispatch_skipped',
                array(
                    'reason'       => $reason_key,
                    'error'        => 'runner_already_active',
                    'wake_pending' => true,
                )
            );

            return array(
                'success'      => false,
                'error'        => 'runner_already_active',
                'wake_pending' => true,
            );
        }

        $use_local_cli             = $this->should_use_local_cli_runner();
        $runner_id                 = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : md5( uniqid( 'niwa_runner_', true ) );
        $transport                 = $use_local_cli ? 'local_cli' : 'web_loopback';
        $state['last_spawned_at']  = time();
        $state['active_until']     = max( intval( isset( $state['active_until'] ) ? $state['active_until'] : 0 ), time() + max( 10, intval( $active_ttl ) ) );
        $state['active_runner_id'] = $runner_id;
        $state['active_transport'] = $transport;
        $state['last_transport']   = $transport;
        $state['last_error']       = '';
        $state['last_spawn_error'] = '';
        $this->telegram->update_runner_state( $state );

        if ( 'local_cli' === $transport ) {
            return $this->dispatch_local_cli_transport( $reason_key, $runner_id, $secret );
        }

        return $this->dispatch_web_loopback_transport( $reason_key, $runner_id, $secret );
    }

    public function should_use_local_cli_runner() {
        $missing = new stdClass();
        $override = $this->call( 'can_spawn_local_cli', $missing );
        if ( $override !== $missing ) {
            return ! empty( $override );
        }

        return class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' )
            && Niwa_AI_Admin_Autonomy_Process_Runtime::can_spawn_local_cli();
    }

    private function dispatch_local_cli_transport( $reason_key, $runner_id, $secret ) {
        $local_cli = $this->dispatch_local_cli_runner( $reason_key, $runner_id );
        if ( ! empty( $local_cli['success'] ) ) {
            $pid = isset( $local_cli['pid'] ) ? max( 0, intval( $local_cli['pid'] ) ) : 0;
            if ( $pid > 0 ) {
                $state               = $this->telegram->get_runner_state();
                $state['active_pid'] = $pid;
                $this->telegram->update_runner_state( $state );
            }

            $this->log(
                'loopback_runner_dispatched',
                array(
                    'reason'    => $reason_key,
                    'transport' => 'local_cli',
                    'pid'       => $pid,
                )
            );

            $local_cli['runner_id'] = $runner_id;
            return $local_cli;
        }

        $error = isset( $local_cli['error'] ) ? (string) $local_cli['error'] : 'local_cli_dispatch_failed';
        $this->log(
            'loopback_runner_local_cli_fallback',
            array(
                'reason'    => $reason_key,
                'transport' => 'local_cli',
                'error'     => $error,
            )
        );

        $this->switch_active_transport( $runner_id, 'web_loopback' );
        $fallback = $this->dispatch_web_loopback_transport( $reason_key, $runner_id, $secret );
        if ( is_array( $fallback ) ) {
            $fallback['fallback_from']   = 'local_cli';
            $fallback['local_cli_error'] = $error;
        }

        return $fallback;
    }

    private function dispatch_web_loopback_transport( $reason_key, $runner_id, $secret ) {
        $url = add_query_arg(
            array(
                'action'    => 'niwa_ai_admin_autonomy_runner',
                'reason'    => sanitize_key( $reason_key ),
                'runner_id' => $runner_id,
            ),
            admin_url( 'admin-post.php' )
        );
        $body              = $this->build_signature_body( 'niwa_runner', $secret );
        $body['runner_id'] = $runner_id;
        // Block briefly so we can observe the acknowledgement. The web handler
        // sends an early 200 ('accepted') and then continues the runner turn in
        // the background, so this returns within milliseconds on success while
        // still surfacing real dispatch failures (403, 5xx, unreachable host).
        $response          = wp_remote_post(
            $url,
            array(
                'timeout'   => 3,
                'blocking'  => true,
				'sslverify' => true,
                'body'      => $body,
            )
        );

        if ( is_wp_error( $response ) ) {
            $error = $response->get_error_message();

            // A timeout means the request was sent but not acknowledged inside
            // the budget. The runner may already be executing in the background,
            // so treat it as fired/unknown rather than tearing down a possibly
            // live runner — the recovery net catches a genuine no-show.
            if ( $this->wp_remote_error_is_timeout( $response ) ) {
                $this->log(
                    'loopback_runner_dispatch_ack_timeout',
                    array(
                        'reason'    => $reason_key,
                        'transport' => 'web_loopback',
                        'error'     => $error,
                    )
                );

                return array(
                    'success'   => true,
                    'transport' => 'web_loopback',
                    'runner_id' => $runner_id,
                    'note'      => 'ack_timeout',
                );
            }

            $this->mark_dispatch_failed( $error, 'web_loopback' );
            $this->log(
                'loopback_runner_dispatch_failed',
                array(
                    'reason'    => $reason_key,
                    'transport' => 'web_loopback',
                    'error'     => $error,
                )
            );

            return array(
                'success'   => false,
                'error'     => $error,
                'transport' => 'web_loopback',
            );
        }

        $status = (int) wp_remote_retrieve_response_code( $response );
        if ( $status < 200 || $status >= 300 ) {
            $error = 'http_' . $status;
            $this->mark_dispatch_failed( $error, 'web_loopback' );
            $this->log(
                'loopback_runner_dispatch_failed',
                array(
                    'reason'    => $reason_key,
                    'transport' => 'web_loopback',
                    'error'     => $error,
                    'http_code' => $status,
                )
            );

            return array(
                'success'   => false,
                'error'     => $error,
                'transport' => 'web_loopback',
            );
        }

        $this->log(
            'loopback_runner_dispatched',
            array(
                'reason'    => $reason_key,
                'transport' => 'web_loopback',
                'http_code' => $status,
            )
        );

        return array(
            'success'   => true,
            'transport' => 'web_loopback',
            'runner_id' => $runner_id,
        );
    }

    private function wp_remote_error_is_timeout( $response ) {
        if ( ! is_wp_error( $response ) ) {
            return false;
        }

        foreach ( (array) $response->get_error_messages() as $message ) {
            $message = strtolower( (string) $message );
            if ( false !== strpos( $message, 'timed out' ) || false !== strpos( $message, 'timeout' ) ) {
                return true;
            }
        }

        return false;
    }

    private function dispatch_local_cli_runner( $reason, $runner_id ) {
        $missing = new stdClass();
        $override = $this->call( 'spawn_local_cli_runner', $missing, array( $reason, $runner_id ) );
        if ( $override !== $missing ) {
            return (array) $override;
        }

        if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Process_Runtime' ) || ! defined( 'NIWA_AI_DIR' ) ) {
            return array(
                'success' => false,
                'error'   => 'local_cli_unavailable',
            );
        }

        $timeout = max( 120, intval( $this->call( 'runner_liveness_ttl', 600 ) ) + 120 );
        if ( defined( 'Niwa_AI_Admin_Autonomy::TURN_WORKER_TIME_LIMIT' ) ) {
            $timeout = max( $timeout, intval( constant( 'Niwa_AI_Admin_Autonomy::TURN_WORKER_TIME_LIMIT' ) ) + 180 );
        }

        return Niwa_AI_Admin_Autonomy_Process_Runtime::spawn_background_php(
            NIWA_AI_DIR . 'bin/telegram-runner.php',
            array(
                'reason'    => sanitize_key( (string) $reason ),
                'runner-id' => (string) $runner_id,
            ),
            defined( 'ABSPATH' ) ? ABSPATH : null,
            $timeout
        );
    }

    private function mark_dispatch_failed( $error, $transport ) {
        $state                     = $this->telegram->get_runner_state();
        $state['active_until']     = 0;
        $state['active_runner_id'] = '';
        $state['active_transport'] = '';
        $state['active_pid']       = 0;
        $state['last_error']       = (string) $error;
        $state['last_spawn_error'] = (string) $error;
        $state['last_transport']   = (string) $transport;
        $this->telegram->update_runner_state( $state );
    }

    private function switch_active_transport( $runner_id, $transport ) {
        $state = $this->telegram->get_runner_state();
        if ( isset( $state['active_runner_id'] ) && (string) $state['active_runner_id'] !== (string) $runner_id ) {
            return;
        }

        $state['active_until']     = max( intval( isset( $state['active_until'] ) ? $state['active_until'] : 0 ), time() + 10 );
        $state['active_runner_id'] = (string) $runner_id;
        $state['active_transport'] = (string) $transport;
        $state['active_pid']       = 0;
        $state['last_transport']   = (string) $transport;
        $state['last_error']       = '';
        $state['last_spawn_error'] = '';
        $this->telegram->update_runner_state( $state );
    }

    private function has_active_actor_messages() {
        return $this->call( 'has_active_actor_messages', false );
    }

    private function clear_stale_buffer_runner_state( $state, $reason ) {
        return (array) $this->call( 'clear_stale_buffer_runner_state', $state, array( $state, $reason ) );
    }

    private function build_signature_body( $prefix, $secret ) {
        return (array) $this->call( 'build_loopback_signature_body', array(), array( $prefix, $secret ) );
    }

    private function log( $event, $context = array() ) {
        $this->call( 'debug_logger', null, array( $event, $context ) );
    }

    private function call( $name, $default = null, $args = array() ) {
        return Niwa_AI_Admin_Autonomy_Callbacks::call( $this->callbacks, $name, $args, $default );
    }
}
