<?php

namespace App\ScheduledTasks;

use App\Events\ScheduledTaskFinished;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Mtc\MercuryDataModels\ScheduledTask;
use Mtc\MercuryDataModels\ScheduledTaskAction;
use Mtc\MercuryDataModels\ScheduledTaskRun;
use Throwable;

class ScheduledTaskRunner
{
    public function __construct(private readonly ActionRegistry $registry)
    {
    }

    /**
     * Execute one run. Safe to call concurrently for the same run or task:
     * the atomic claim is the enforcement point for both.
     */
    public function execute(ScheduledTaskRun $run): void
    {
        if (!$this->claim($run)) {
            $this->applyOverlapPolicy($run->fresh());
            return;
        }

        $run = $run->fresh();
        $task = $run->task()->with('actions')->first();

        if (!$task) {
            $this->finish($run, null, ScheduledTaskRun::STATUS_FAILED, [], 'Task no longer exists');
            return;
        }

        $context = new TaskRunContext($run);

        foreach ($task->actions as $action) {
            $context->push($action->action_type, $this->executeAction($action, $context));
        }

        $failed = $context->hasFailure();
        $error = collect($context->entries())
            ->first(fn (array $entry) => $entry['status'] === ActionResult::STATUS_FAILED)['message'] ?? null;

        $this->finish(
            $run,
            $task,
            $failed ? ScheduledTaskRun::STATUS_FAILED : ScheduledTaskRun::STATUS_COMPLETED,
            $context->entries(),
            $error
        );
    }

    /**
     * Mark a run as failed from the job's failed() handler (worker death, hard timeout).
     */
    public function markFailed(ScheduledTaskRun $run, string $reason): void
    {
        $run = $run->fresh();

        if ($run && !$run->isFinished()) {
            $this->finish($run, $run->task, ScheduledTaskRun::STATUS_FAILED, $run->action_results ?? [], $reason);
        }
    }

    /**
     * Atomically move the run from pending to running, refusing when any other run
     * of the same task is already running. This, not the dispatcher's pre-check,
     * is what makes same-task concurrency impossible.
     */
    private function claim(ScheduledTaskRun $run): bool
    {
        return DB::update(
            'UPDATE scheduled_task_runs SET status = ?, started_at = ?
             WHERE id = ? AND status = ?
               AND NOT EXISTS (
                   SELECT 1 FROM (
                       SELECT id FROM scheduled_task_runs
                       WHERE scheduled_task_id = ? AND status = ? AND id != ?
                   ) AS running_sibling
               )',
            [
                ScheduledTaskRun::STATUS_RUNNING,
                Carbon::now(),
                $run->id,
                ScheduledTaskRun::STATUS_PENDING,
                $run->scheduled_task_id,
                ScheduledTaskRun::STATUS_RUNNING,
                $run->id,
            ]
        ) === 1;
    }

    /**
     * The claim failed. If that was because a sibling run is still executing,
     * apply the task's overlap policy; otherwise the run was already handled.
     */
    private function applyOverlapPolicy(?ScheduledTaskRun $run): void
    {
        if (!$run || $run->status !== ScheduledTaskRun::STATUS_PENDING) {
            return;
        }

        if ($run->task?->overlap_policy === ScheduledTask::OVERLAP_QUEUE) {
            // Leave pending; the dispatcher picks it up after the sibling finishes
            return;
        }

        $run->markSkipped('Previous run of this task still running');
    }

    private function executeAction(ScheduledTaskAction $action, TaskRunContext $context): ActionResult
    {
        if (!$action->shouldRun($context->hasFailure())) {
            return ActionResult::skipped('Condition not met (' . $action->run_when . ')');
        }

        $handler = $this->registry->resolve($action->action_type);

        if (!$handler) {
            return ActionResult::failed("Unknown action type '{$action->action_type}'");
        }

        if (!$handler->available()) {
            return ActionResult::failed('Integration disabled for this action');
        }

        $timeout = $action->timeout_seconds ?? config('scheduled_tasks.defaults.action_timeout');
        $startedAt = microtime(true);

        try {
            $result = $handler->execute($action->config ?? [], $context);
        } catch (Throwable $exception) {
            Log::error('Scheduled task action failed', [
                'action' => $action->action_type,
                'task_id' => $action->scheduled_task_id,
                'exception' => $exception,
            ]);
            $result = ActionResult::failed($exception->getMessage());
        }

        $durationSeconds = microtime(true) - $startedAt;

        if (!$result->failedStatus() && $durationSeconds > $timeout) {
            $result = ActionResult::failed(
                sprintf('Timed out (took %ds, budget %ds)', (int) $durationSeconds, $timeout),
                $result->stats
            );
        }

        return new ActionResult(
            $result->status,
            $result->message,
            array_merge($result->stats, ['duration_ms' => (int) round($durationSeconds * 1000)])
        );
    }

    private function finish(
        ScheduledTaskRun $run,
        ?ScheduledTask $task,
        string $status,
        array $actionResults,
        ?string $error
    ): void {
        $run->update([
            'status' => $status,
            'finished_at' => Carbon::now(),
            'action_results' => $actionResults,
            'error' => $error,
        ]);

        $task?->update(['last_run_at' => Carbon::now()]);

        if ($task && $status === ScheduledTaskRun::STATUS_FAILED) {
            $this->alertOnRepeatedFailure($task);
        }

        if ($task) {
            Event::dispatch(new ScheduledTaskFinished($task, $run->fresh()));
        }
    }

    /**
     * Two consecutive failed runs of the same task alert the developers,
     * mirroring the QueueProcessLog pattern.
     */
    private function alertOnRepeatedFailure(ScheduledTask $task): void
    {
        $recent = $task->runs()
            ->whereIn('status', [ScheduledTaskRun::STATUS_COMPLETED, ScheduledTaskRun::STATUS_FAILED])
            ->latest('id')
            ->take(2)
            ->pluck('status');

        $developerEmail = config('mail.developer_alerts');

        if (
            empty($developerEmail)
            || $recent->count() < 2
            || $recent->contains(ScheduledTaskRun::STATUS_COMPLETED)
        ) {
            return;
        }

        Mail::raw(
            sprintf(
                "Scheduled task \"%s\" (tenant %s) has failed twice in a row.\nLast error: %s",
                $task->name,
                tenant('id'),
                $task->runs()->latest('id')->first()?->error ?? 'unknown'
            ),
            fn ($message) => $message
                ->to($developerEmail)
                ->subject('Scheduled task failing repeatedly: ' . $task->name)
        );
    }
}
