<?php

namespace App\ScheduledTasks;

use App\Jobs\RunScheduledTaskJob;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Mtc\MercuryDataModels\ScheduledTask;
use Mtc\MercuryDataModels\ScheduledTaskAction;
use Mtc\MercuryDataModels\ScheduledTaskRun;

class ScheduledTaskRepository
{
    public function __construct(
        private readonly ScheduledTaskClock $clock,
        private readonly SlotLoadService $slotLoad,
    ) {
    }

    public function create(array $data): ScheduledTask
    {
        $task = ScheduledTask::query()->create(array_merge($data, [
            'managed_by' => ScheduledTask::MANAGED_BY_ADMIN,
            'created_by' => Auth::id(),
        ]));

        $this->afterScheduleChange($task);

        return $task;
    }

    public function update(ScheduledTask $task, array $data, array $actions): ScheduledTask
    {
        // Non-schedule triggers carry no schedule rules; clear any left over from a
        // previous trigger type so the record stays clean.
        if (($data['trigger_type'] ?? null) !== ScheduledTask::TRIGGER_SCHEDULE) {
            $data['schedule_rules'] = null;
        }

        DB::transaction(function () use ($task, $data, $actions) {
            $task->update($data);
            $this->syncActions($task, $actions);
        });

        $this->afterScheduleChange($task->fresh());

        return $task->fresh(['actions']);
    }

    public function delete(ScheduledTask $task): void
    {
        $task->runs()
            ->where('status', ScheduledTaskRun::STATUS_PENDING)
            ->each(fn (ScheduledTaskRun $run) => $run->markSkipped('Task deleted'));

        $task->delete();
        $this->slotLoad->rebuildForCurrentTenant();
    }

    /**
     * "Run now": an ad-hoc run row, dispatched immediately.
     */
    public function runNow(ScheduledTask $task): ScheduledTaskRun
    {
        $run = $task->runs()->create([
            'status' => ScheduledTaskRun::STATUS_PENDING,
            'scheduled_for' => Carbon::now(),
            'trigger' => ScheduledTaskRun::TRIGGER_MANUAL,
            'metadata' => ['user_id' => Auth::id()],
        ]);

        RunScheduledTaskJob::dispatch($run);

        return $run;
    }

    /**
     * Retry a failed run as a new, linked run.
     */
    public function retry(ScheduledTaskRun $failedRun): ScheduledTaskRun
    {
        $run = $failedRun->task->runs()->create([
            'status' => ScheduledTaskRun::STATUS_PENDING,
            'scheduled_for' => Carbon::now(),
            'trigger' => ScheduledTaskRun::TRIGGER_RETRY,
            'triggered_by_run_id' => $failedRun->id,
            'metadata' => ['user_id' => Auth::id()],
        ]);

        RunScheduledTaskJob::dispatch($run);

        return $run;
    }

    /**
     * Walk the ancestry of a proposed parent; the task may not appear in it
     * and the chain may not exceed the depth cap.
     */
    public function parentCreatesCycle(ScheduledTask $task, ?int $parentTaskId): bool
    {
        $depthCap = 5;
        $seen = [$task->id];
        $cursor = $parentTaskId;

        while ($cursor !== null && $depthCap-- > 0) {
            if (in_array($cursor, $seen, true)) {
                return true;
            }

            $seen[] = $cursor;
            $cursor = ScheduledTask::query()->find($cursor)?->parent_task_id;
        }

        return $cursor !== null && $depthCap < 0;
    }

    /**
     * Tasks eligible as a parent for the given task: not itself, not its descendants.
     */
    public function eligibleParents(?ScheduledTask $task)
    {
        return ScheduledTask::query()
            ->when($task, function ($query) use ($task) {
                $descendants = $this->descendantIds($task);
                $query->whereNotIn('id', array_merge([$task->id], $descendants));
            })
            ->orderBy('name')
            ->get(['id', 'name']);
    }

    private function descendantIds(ScheduledTask $task): array
    {
        $ids = [];
        $frontier = [$task->id];
        $guard = 0;

        while (!empty($frontier) && $guard++ < 20) {
            $frontier = ScheduledTask::query()
                ->whereIn('parent_task_id', $frontier)
                ->pluck('id')
                ->toArray();
            $ids = array_merge($ids, $frontier);
        }

        return $ids;
    }

    private function syncActions(ScheduledTask $task, array $actions): void
    {
        $rows = array_values($actions);
        $keptIds = [];

        // Pass 1: park every kept/created row at a high order so reordering
        // can never trip the unique (task_id, order) key mid-sync
        foreach ($rows as $index => $actionData) {
            $parkedOrder = $index + 1001;

            $action = !empty($actionData['id'])
                ? $task->actions()->whereKey($actionData['id'])->first()
                : null;

            if ($action) {
                $action->update(['order' => $parkedOrder]);
            } else {
                $action = $task->actions()->create([
                    'order' => $parkedOrder,
                    'action_type' => $actionData['action_type'],
                ]);
            }

            $keptIds[$parkedOrder] = $action->id;
        }

        $task->actions()->whereNotIn('id', $keptIds)->delete();

        // Pass 2: settle final order and attributes. The first action has no prior step,
        // so its condition is always forced to "always" (an on_failure first step would
        // otherwise never run).
        foreach ($rows as $index => $actionData) {
            $task->actions()->whereKey($keptIds[$index + 1001])->update([
                'order' => $index + 1,
                'action_type' => $actionData['action_type'],
                'config' => json_encode($actionData['config'] ?? []),
                'run_when' => $index === 0
                    ? ScheduledTaskAction::RUN_WHEN_ALWAYS
                    : ($actionData['run_when'] ?? ScheduledTaskAction::RUN_WHEN_ON_SUCCESS),
                'timeout_seconds' => $actionData['timeout_seconds'] ?? null,
            ]);
        }
    }

    /**
     * Recompute next_due_at, refresh the day's remaining queue and the slot profile.
     * Public so callers that change a task's schedule directly (e.g. the stock-sync
     * frequency listener) can trigger the same reconciliation.
     */
    public function refreshSchedule(ScheduledTask $task): void
    {
        $this->afterScheduleChange($task);
    }

    private function afterScheduleChange(ScheduledTask $task): void
    {
        $task->update(['next_due_at' => $this->clock->nextDueAt($task, Carbon::now())]);

        if ($task->enabled) {
            // Remove today's not-yet-due pending runs and re-materialise from the new schedule
            $task->runs()
                ->where('status', ScheduledTaskRun::STATUS_PENDING)
                ->where('trigger', ScheduledTaskRun::TRIGGER_SCHEDULE)
                ->where('scheduled_for', '>', Carbon::now())
                ->delete();

            $this->clock->occurrencesForDate($task, Carbon::today())
                ->filter(fn (Carbon $occurrence) => $occurrence->greaterThan(Carbon::now()))
                ->each(fn (Carbon $occurrence) => ScheduledTaskRun::query()->insertOrIgnore([
                    'scheduled_task_id' => $task->id,
                    'status' => ScheduledTaskRun::STATUS_PENDING,
                    'scheduled_for' => $occurrence,
                    'trigger' => ScheduledTaskRun::TRIGGER_SCHEDULE,
                    'created_at' => Carbon::now(),
                    'updated_at' => Carbon::now(),
                ]));
        } else {
            // Pausing: today's remaining pending runs are skipped with the reason
            $task->runs()
                ->where('status', ScheduledTaskRun::STATUS_PENDING)
                ->each(fn (ScheduledTaskRun $run) => $run->markSkipped('Task paused'));
        }

        $this->slotLoad->rebuildForCurrentTenant();
    }
}
