<?php

/**
 * LolaCore — Remote Execution Service
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Hub;

use LolaCore\Actions\ActionExecutor;
use LolaCore\Actions\ActionLogger;
use WP_Error;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Orchestrates LolaHub remote ability execution on the child (sprint C1).
 *
 * Validation order matches sprint-c1-remote-execution-channel.md:
 * identity → connected → idempotency → allow list → scope → dry_run/fingerprint → dispatch → log.
 */
final class RemoteExecutionService
{
    /**
     * Meta abilities that are not bridged chat actions.
     *
     * @var string[]
     */
    private const META_ABILITIES = [
        'lolacore/get-site-info',
        'lolacore/get-site-health',
        'lolacore/get-memory-context',
        'lolacore/add-memory-fact',
        'lolacore/get-action-history',
        'lolacore/get-scan-data',
        'lolacore/chat',
        'lolacore/execute-action',
        'lolacore/add-skill',
        'lolacore/apply-agency-profile',
        'lolacore/set-provider',
        'lolacore/receive-memory-reconcile',
        'lolacore/read-site-memory',
    ];

    /**
     * Hub meta writes approved at the master — no state fingerprint in v1 (C3).
     *
     * @var string[]
     */
    private const FINGERPRINT_EXEMPT_META_ABILITIES = [
        'lolacore/apply-agency-profile',
        'lolacore/set-provider',
        'lolacore/receive-memory-reconcile',
    ];

    public function __construct(
        private ?PairingRegistrar $pairing = null,
        private ?RemoteAbilityAllowList $allowList = null,
        private ?HubScope $scope = null,
        private ?HubFingerprint $fingerprint = null,
        private ?HubRequestCache $requestCache = null,
    ) {
        $this->pairing       = $pairing ?? new PairingRegistrar();
        $this->allowList     = $allowList ?? new RemoteAbilityAllowList();
        $this->scope         = $scope ?? new HubScope($this->allowList);
        $this->fingerprint   = $fingerprint ?? new HubFingerprint();
        $this->requestCache  = $requestCache ?? new HubRequestCache();
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>|WP_Error
     */
    public function execute(string $ability, array $params, string $requestId, bool $dryRun = false, ?string $expectedFingerprint = null): array|WP_Error
    {
        if (!$this->pairing->isAuthenticatedMaster()) {
            return new WP_Error(
                'lolacore_hub_unauthorized',
                __('Remote execution requires an authenticated LolaHub master Application Password.', 'lolacore'),
                ['status' => 403]
            );
        }

        if (!$this->pairing->isConnected()) {
            return new WP_Error(
                'lolacore_hub_disconnected',
                __('This site is not connected to a LolaHub master.', 'lolacore'),
                ['status' => 403]
            );
        }

        if ($requestId === '') {
            return new WP_Error(
                'lolacore_hub_missing_request_id',
                __('A request_id is required for remote execution.', 'lolacore'),
                ['status' => 400]
            );
        }

        $cached = $this->requestCache->get($requestId);
        if ($cached !== null) {
            return $cached;
        }

        if (!$this->allowList->isAllowed($ability)) {
            return new WP_Error(
                'lolacore_hub_ability_not_permitted',
                sprintf(
                    /* translators: %s: ability name */
                    __('The ability "%s" is not permitted for remote execution.', 'lolacore'),
                    $ability
                ),
                ['status' => 403]
            );
        }

        if (!$this->scope->isAllowed($ability, $params)) {
            return new WP_Error(
                'lolacore_hub_ability_outside_scope',
                sprintf(
                    /* translators: %s: ability name */
                    __('The ability "%s" is outside the scope this site granted to the master.', 'lolacore'),
                    $ability
                ),
                ['status' => 403]
            );
        }

        if ($dryRun) {
            return $this->buildDryRunResponse($ability, $params, $requestId);
        }

        if ($this->isReadOnlyAbility($ability) || in_array($ability, self::FINGERPRINT_EXEMPT_META_ABILITIES, true)) {
            return $this->finalizeExecution($ability, $params, $requestId, null);
        }

        $verifiable = $this->fingerprint->isVerifiable($ability, $params);

        if (!$verifiable) {
            // Master approval + params frozen at dry-run T0 (UI-4 §3.3 — generative/create flows).
            return $this->finalizeExecution($ability, $params, $requestId, null);
        }

        $currentFingerprint = $this->fingerprint->compute($ability, $params);

        if ($currentFingerprint === null) {
            return [
                'request_id'    => $requestId,
                'ability'       => $ability,
                'status'        => 'not_verifiable',
                'error_message' => __('Unable to compute a state fingerprint for this ability.', 'lolacore'),
                'reversible'    => false,
            ];
        }

        if ($expectedFingerprint === null || $expectedFingerprint === '') {
            return new WP_Error(
                'lolacore_hub_missing_fingerprint',
                __('An expected_fingerprint from the dry-run preview is required before applying this change.', 'lolacore'),
                ['status' => 400]
            );
        }

        if (!hash_equals($currentFingerprint, $expectedFingerprint)) {
            $previewResponse = $this->buildDryRunResponse($ability, $params, $requestId);
            $previewResponse['status'] = 'drifted';

            return $previewResponse;
        }

        return $this->finalizeExecution($ability, $params, $requestId, $currentFingerprint);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>|WP_Error
     */
    private function finalizeExecution(string $ability, array $params, string $requestId, ?string $fingerprint): array|WP_Error
    {
        $dispatch = $this->dispatchAbility($ability, $params, false);
        if ($dispatch instanceof WP_Error) {
            return $dispatch;
        }

        $logId = $this->logExecution($ability, $params, $dispatch, $requestId);

        $response = [
            'request_id'  => $requestId,
            'ability'     => $ability,
            'status'      => 'ok',
            'result'      => $dispatch,
            'reversible'  => $this->isReversibleAbility($ability, $params),
            'log_id'      => $logId,
        ];

        if ($fingerprint !== null) {
            $response['fingerprint'] = $fingerprint;
        }

        $this->requestCache->put($requestId, $response);

        return $response;
    }

    private function isReadOnlyAbility(string $ability): bool
    {
        if (function_exists('wp_get_ability')) {
            $abilityObject = wp_get_ability($ability);
            if ($abilityObject instanceof \WP_Ability) {
                $meta = $abilityObject->get_meta();
                if (! empty($meta['annotations']['readonly'])) {
                    return true;
                }
            }
        }

        return in_array($ability, [
            'lolacore/get-site-info',
            'lolacore/get-site-health',
            'lolacore/get-memory-context',
            'lolacore/get-action-history',
            'lolacore/get-scan-data',
            'lolacore/read-site-memory',
        ], true);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function buildDryRunResponse(string $ability, array $params, string $requestId): array
    {
        $preview = $this->dispatchAbility($ability, $params, true);
        if ($preview instanceof WP_Error) {
            return [
                'request_id'    => $requestId,
                'ability'       => $ability,
                'status'        => 'error',
                'error_message' => $preview->get_error_message(),
                'reversible'    => false,
            ];
        }

        $fingerprint  = $this->fingerprint->compute($ability, $params);
        $verifiable   = $this->fingerprint->isVerifiable($ability, $params);

        return [
            'request_id'  => $requestId,
            'ability'     => $ability,
            'status'      => 'ok',
            'preview'     => $preview,
            'fingerprint' => $fingerprint,
            'verifiable'  => $verifiable,
            'reversible'  => $this->isReversibleAbility($ability, $params),
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>|WP_Error
     */
    private function dispatchAbility(string $ability, array $params, bool $dryRun): array|WP_Error
    {
        if ($ability === 'lolacore/execute-action') {
            $executor   = new ActionExecutor();
            $actionId   = (string) ($params['action_id'] ?? '');
            $actionArgs = (array) ($params['params'] ?? []);

            if ($actionId === '') {
                return new WP_Error(
                    'lolacore_hub_invalid_params',
                    __('execute-action requires an action_id parameter.', 'lolacore'),
                    ['status' => 400]
                );
            }

            return $executor->execute($actionId, $actionArgs, $dryRun, 'hub');
        }

        $bridgedActionId = $this->resolveBridgedActionId($ability);
        if ($bridgedActionId !== null) {
            $executor = new ActionExecutor();

            return $executor->execute($bridgedActionId, $params, $dryRun, 'hub');
        }

        if (!function_exists('wp_get_ability')) {
            return new WP_Error(
                'lolacore_hub_abilities_unavailable',
                __('The WordPress Abilities API is not available.', 'lolacore'),
                ['status' => 503]
            );
        }

        $abilityObject = wp_get_ability($ability);
        if (!$abilityObject instanceof \WP_Ability) {
            return new WP_Error(
                'lolacore_hub_unknown_ability',
                sprintf(
                    /* translators: %s: ability name */
                    __('The ability "%s" is not registered.', 'lolacore'),
                    $ability
                ),
                ['status' => 404]
            );
        }

        $abilityParams = $params;
        if ($dryRun && in_array($ability, self::FINGERPRINT_EXEMPT_META_ABILITIES, true)) {
            $abilityParams['dry_run'] = true;
        }

        $output = $abilityObject->execute($abilityParams);
        if ($output instanceof WP_Error) {
            return $output;
        }

        return is_array($output) ? $output : ['result' => $output];
    }

    private function resolveBridgedActionId(string $ability): ?string
    {
        if (!str_starts_with($ability, 'lolacore/')) {
            return null;
        }

        if (in_array($ability, self::META_ABILITIES, true)) {
            return null;
        }

        $suffix = substr($ability, strlen('lolacore/'));

        return ActionExecutor::normalizeActionId(str_replace('-', '_', $suffix));
    }

    /**
     * @param array<string, mixed> $params
     */
    private function isReversibleAbility(string $ability, array $params): bool
    {
        $registry = (new ActionExecutor())->getActionRegistry();
        $actionId = $this->resolveBridgedActionId($ability);

        if ($ability === 'lolacore/execute-action') {
            $actionId = ActionExecutor::normalizeActionId((string) ($params['action_id'] ?? ''));
        }

        if ($actionId === null || $actionId === '') {
            return false;
        }

        return ($registry[$actionId]['type'] ?? '') === 'write';
    }

    /**
     * @param array<string, mixed> $params
     * @param array<string, mixed> $result
     */
    private function logExecution(string $ability, array $params, array $result, string $requestId): int
    {
        $logger = new ActionLogger();
        $nodeId = $this->pairing->getNodeId();
        $label  = $this->pairing->getMasterLabel();
        $source = $label !== '' ? 'hub:' . $label : 'hub:' . $nodeId;

        $sanitizedParams = $this->sanitizeParamsForLog($params);

        return $logger->log(
            $ability,
            sprintf('Remote hub execution: %s', $ability),
            'success',
            $source,
            [
                'request_id' => $requestId,
                'ability'    => $ability,
                'params'     => $sanitizedParams,
                'result'     => $this->sanitizeParamsForLog($result),
            ]
        );
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function sanitizeParamsForLog(array $params): array
    {
        $clean = [];

        foreach ($params as $key => $value) {
            if (!is_string($key)) {
                continue;
            }

            if (is_string($value)) {
                $clean[$key] = strlen($value) > 500 ? substr($value, 0, 500) . '…[truncated]' : $value;
                continue;
            }

            if (is_array($value)) {
                $encoded = wp_json_encode($value);
                $clean[$key] = is_string($encoded) && strlen($encoded) > 500
                    ? substr($encoded, 0, 500) . '…[truncated]'
                    : $value;
                continue;
            }

            $clean[$key] = $value;
        }

        return $clean;
    }
}
