<?php

namespace FSPoster\App\Pages\Settings\Workflow\Services;

use FSPoster\App\Pages\Settings\Workflow\DTOs\Response\WorkflowActionResponse;
use FSPoster\App\Pages\Settings\Workflow\Repositories\WorkflowQueueRepository;
use FSPoster\App\Providers\Common\WorkflowEventsManager;
use FSPoster\App\Providers\Core\Container;
use FSPoster\App\Providers\DB\Collection;

class WorkflowQueueRunner
{
    private const DEFAULT_MAX_ATTEMPTS = 3;
    private const DEFAULT_STALE_LOCK_SECONDS = 300;
    private const DEFAULT_RETRY_DELAY_SECONDS = 300;
    private const DEFAULT_BATCH_LIMIT = 50;
    private const DEFAULT_MAX_CHAIN_DEPTH = 10;
    private const DEFAULT_MAX_AGE_DAYS = 7;

    private WorkflowQueueRepository $repository;
    private WorkflowEventsManager $workflowEventsManager;
    private WorkflowQueueService $workflowQueueService;

    public function __construct(
        WorkflowQueueRepository $repository,
        WorkflowEventsManager $workflowEventsManager,
        WorkflowQueueService $workflowQueueService
    ) {
        $this->repository = $repository;
        $this->workflowEventsManager = $workflowEventsManager;
        $this->workflowQueueService = $workflowQueueService;
    }

    public function runDue(int $limit = 0): void
    {
        $lockToken = wp_generate_uuid4();
        $batchLimit = $limit > 0 ? $limit : $this->getBatchLimit();
        $schedules = $this->repository->claimDue($lockToken, $batchLimit, $this->getStaleLockSeconds(), $this->getMaxAgeDays());

        if (empty($schedules)) {
            return;
        }

        foreach ($schedules as $schedule) {
            $this->runSingle($schedule);
        }
    }

    private function runSingle(Collection $schedule): void
    {
        $chainDepth = (int)($schedule['chain_depth'] ?? 0);

        if ($chainDepth > $this->getMaxChainDepth()) {
            $this->finishCanceled(
                $schedule,
                fsp__('Workflow chain depth limit exceeded at depth %s.', [$chainDepth]),
                'error'
            );

            return;
        }

        $attemptCount = (int)$schedule['attempt_count'] + 1;
        $this->repository->touchAttempt((int)$schedule['id'], $attemptCount);

        $workflow = $this->repository->findWorkflow((int)$schedule['workflow_id']);
        $workflowAction = !empty($schedule['workflow_action_id'])
            ? $this->repository->findWorkflowAction((int)$schedule['workflow_action_id'])
            : null;

        if ($workflow === null || (isset($workflow['is_active']) && !$workflow['is_active'])) {
            $this->finishCanceled($schedule, fsp__('Workflow is inactive or missing.'));
            return;
        }

        if (!empty($schedule['workflow_action_id']) && ($workflowAction === null || (isset($workflowAction['is_active']) && !$workflowAction['is_active']))) {
            $this->finishCanceled($schedule, fsp__('Workflow action is inactive or missing.'));
            return;
        }

        $driver = $this->workflowEventsManager->getDriverManager()->get($schedule['driver']);

        if ($driver === null) {
            $this->finishCanceled(
                $schedule,
                fsp__('Workflow driver not found: %s', [$schedule['driver']]),
                'canceled'
            );
            return;
        }

        $eventData = json_decode($schedule['event_data'] ?? '[]', true) ?: [];
        $actionData = json_decode($schedule['data'] ?? '[]', true) ?: [];

        $actionSettings = Container::get(WorkflowActionResponse::class);
        $actionSettings->setId((int)$schedule['workflow_action_id']);
        $actionSettings->setWorkflowId((int)$schedule['workflow_id']);
        $actionSettings->setSlug((string)$schedule['when']);
        $actionSettings->setDriver((string)$schedule['driver']);
        $actionSettings->setData($actionData);

        $status = 'success';
        $errorMessage = null;

        try {
            $this->workflowQueueService->beginExecutionContext(
                (int)$schedule['id'],
                isset($schedule['origin_schedule_id']) ? (int)$schedule['origin_schedule_id'] : null,
                $chainDepth
            );
            $driver->handle($eventData, $actionSettings, $this->workflowEventsManager->getShortcodeService());
        } catch (\Throwable $throwable) {
            $status = 'error';
            $errorMessage = $throwable->getMessage();
        } finally {
            $this->workflowQueueService->clearExecutionContext();
        }

        if ($status === 'error') {
            $this->finishError($schedule, $attemptCount, $errorMessage);
            return;
        }

        $this->repository->markCompleted((int)$schedule['id'], 'success');
    }

    private function finishCanceled(Collection $schedule, string $message, string $scheduleStatus = 'canceled'): void
    {
        $this->repository->markCanceled((int)$schedule['id'], $message, $scheduleStatus);
    }

    private function finishError(Collection $schedule, int $attemptCount, ?string $message): void
    {
        if ($attemptCount < $this->getMaxAttempts()) {
            $this->repository->markRetry((int)$schedule['id'], $attemptCount, $this->getRetryDelaySeconds(), $message);
            return;
        }

        $this->repository->markCompleted((int)$schedule['id'], 'error', $message);
    }

    private function getMaxChainDepth(): int
    {
        $configuredDepth = (int)apply_filters('fsp_workflow_max_chain_depth', self::DEFAULT_MAX_CHAIN_DEPTH);

        return max(1, $configuredDepth);
    }

    private function getMaxAttempts(): int
    {
        $configured = (int)apply_filters('fsp_workflow_max_attempts', self::DEFAULT_MAX_ATTEMPTS);

        return max(1, $configured);
    }

    private function getRetryDelaySeconds(): int
    {
        $configured = (int)apply_filters('fsp_workflow_retry_delay_seconds', self::DEFAULT_RETRY_DELAY_SECONDS);

        return max(1, $configured);
    }

    private function getStaleLockSeconds(): int
    {
        $configured = (int)apply_filters('fsp_workflow_stale_lock_seconds', self::DEFAULT_STALE_LOCK_SECONDS);

        return max(30, $configured);
    }

    private function getBatchLimit(): int
    {
        $configured = (int)apply_filters('fsp_workflow_runner_batch_limit', self::DEFAULT_BATCH_LIMIT);

        return max(1, $configured);
    }

    private function getMaxAgeDays(): int
    {
        $configured = (int)apply_filters('fsp_workflow_max_age_days', self::DEFAULT_MAX_AGE_DAYS);

        return max(1, $configured);
    }
}
