<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Mtc\Crm\Models\EnquiryAnswer;
use Mtc\MercuryDataModels\Enquiry;

/**
 * Retro-actively move legacy form-submission upload files into the tenant-prefixed path on the
 * shared file-storage disk, and update the stored references in enquiry details and answers.
 *
 * Legacy uploads were stored at "uploads/Y-M/<file>" with no tenant segregation. New uploads are
 * written to "<tenant_id>/uploads/Y-M/<file>" (see EnquiryFormHandler in frontend-api, AUTO-1631).
 *
 * Runs for the currently initialised tenant. To process every tenant, wrap it with the
 * tenants:run command, e.g. `artisan tenants:run "enquiries:migrate-files-to-tenant-path"`.
 */
class MigrateEnquiryFilesToTenantPath extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'enquiries:migrate-files-to-tenant-path
        {--dry-run : List the files that would be moved without moving them}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Move legacy enquiry upload files into the tenant-prefixed path on the file-storage disk';

    /**
     * The disk holding form submission uploads.
     */
    private const DISK = 'file-storage';

    /**
     * The legacy (non tenant-prefixed) upload path prefix.
     */
    private const LEGACY_PREFIX = 'uploads/';

    private bool $dryRun = false;

    private int $filesMoved = 0;

    private int $filesMissing = 0;

    private int $enquiriesUpdated = 0;

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle(): int
    {
        if (empty(tenant('id'))) {
            $this->error('Please run for a tenant (e.g. via `tenants:run "' . $this->getName() . '"`).');
            return self::FAILURE;
        }

        $this->dryRun = (bool) $this->option('dry-run');
        $prefix = tenant('id') . '/';

        if ($this->dryRun) {
            $this->info('Dry run - no files will be moved and no records will be updated.');
        }

        Enquiry::query()
            ->where(function ($query) {
                // Broad pre-filter only; details is JSON-encoded (slashes escaped), so the precise
                // "uploads/" match happens in PHP against the decoded answers.
                $query->where('details', 'like', '%uploads%')
                    ->orWhereHas('answers', fn ($answers) => $answers->where('answer_type', 'file'));
            })
            ->with('answers')
            ->chunkById(100, fn (Collection $enquiries) => $enquiries->each(
                fn (Enquiry $enquiry) => $this->migrateEnquiry($enquiry, $prefix)
            ));

        $this->info(sprintf(
            'Tenant %s: %d file(s) %s, %d enquiry record(s) updated, %d file(s) missing.',
            tenant('id'),
            $this->filesMoved,
            $this->dryRun ? 'to move' : 'moved',
            $this->enquiriesUpdated,
            $this->filesMissing
        ));

        return self::SUCCESS;
    }

    /**
     * Move every legacy upload referenced by the enquiry and update its stored references.
     *
     * @param Enquiry $enquiry
     * @param string $prefix
     * @return void
     */
    private function migrateEnquiry(Enquiry $enquiry, string $prefix): void
    {
        $map = $this->buildMigrationMap($this->legacyPathsFor($enquiry), $prefix);

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

        $detailsChanged = $this->rewriteDetails($enquiry, $map);
        $answersChanged = $this->rewriteAnswers($enquiry, $map);

        if ($detailsChanged || $answersChanged) {
            $this->enquiriesUpdated++;
        }
    }

    /**
     * Collect the unique legacy (non tenant-prefixed) upload paths referenced by the enquiry.
     *
     * @param Enquiry $enquiry
     * @return array<int, string>
     */
    private function legacyPathsFor(Enquiry $enquiry): array
    {
        $paths = [];

        foreach ((array) $enquiry->details as $detail) {
            $this->collectPaths($detail['answer'] ?? null, $paths);
        }

        foreach ($enquiry->answers as $answer) {
            $this->collectPaths($answer->answer_array ?? $answer->answer_value, $paths);
        }

        return array_values(array_unique($paths));
    }

    /**
     * Recursively collect legacy upload paths from a stored answer value.
     *
     * @param mixed $value
     * @param array<int, string> $paths
     * @return void
     */
    private function collectPaths(mixed $value, array &$paths): void
    {
        if (is_array($value)) {
            foreach ($value as $item) {
                $this->collectPaths($item, $paths);
            }
            return;
        }

        if (is_string($value) && str_starts_with($value, self::LEGACY_PREFIX)) {
            $paths[] = $value;
        }
    }

    /**
     * Move each legacy file to its tenant-prefixed location and return an old => new path map of
     * the references that are safe to rewrite (file confirmed present at the new location).
     *
     * @param array<int, string> $paths
     * @param string $prefix
     * @return array<string, string>
     */
    private function buildMigrationMap(array $paths, string $prefix): array
    {
        $disk = Storage::disk(self::DISK);
        $map = [];

        foreach ($paths as $old) {
            $new = $prefix . $old;

            if ($disk->exists($new)) {
                $map[$old] = $new;
                continue;
            }

            if (!$disk->exists($old)) {
                $this->warn("  Missing file, reference left unchanged: {$old}");
                $this->filesMissing++;
                continue;
            }

            if (!$this->dryRun) {
                $disk->move($old, $new);
            }

            $map[$old] = $new;
            $this->filesMoved++;
            $this->line("  " . ($this->dryRun ? 'Would move' : 'Moved') . ": {$old} -> {$new}");
        }

        return $map;
    }

    /**
     * Rewrite the enquiry details JSON using the migration map.
     *
     * @param Enquiry $enquiry
     * @param array<string, string> $map
     * @return bool Whether any reference changed.
     */
    private function rewriteDetails(Enquiry $enquiry, array $map): bool
    {
        $details = (array) $enquiry->details;

        $rewritten = array_map(function ($detail) use ($map) {
            if (is_array($detail) && array_key_exists('answer', $detail)) {
                $detail['answer'] = $this->applyMap($detail['answer'], $map);
            }
            return $detail;
        }, $details);

        if ($rewritten === $details) {
            return false;
        }

        if (!$this->dryRun) {
            $enquiry->details = $rewritten;
            $enquiry->save();
        }

        return true;
    }

    /**
     * Rewrite the enquiry_answers rows using the migration map.
     *
     * @param Enquiry $enquiry
     * @param array<string, string> $map
     * @return bool Whether any reference changed.
     */
    private function rewriteAnswers(Enquiry $enquiry, array $map): bool
    {
        $changed = false;

        foreach ($enquiry->answers as $answer) {
            /** @var EnquiryAnswer $answer */
            $answerChanged = false;

            if (is_string($answer->answer_value) && isset($map[$answer->answer_value])) {
                $answer->answer_value = $map[$answer->answer_value];
                $answerChanged = true;
            }

            if (is_array($answer->answer_array)) {
                $rewritten = $this->applyMap($answer->answer_array, $map);
                if ($rewritten !== $answer->answer_array) {
                    $answer->answer_array = $rewritten;
                    $answerChanged = true;
                }
            }

            if ($answerChanged) {
                $changed = true;
                if (!$this->dryRun) {
                    $answer->save();
                }
            }
        }

        return $changed;
    }

    /**
     * Replace any mapped legacy path within a stored answer value (string or array).
     *
     * @param mixed $value
     * @param array<string, string> $map
     * @return mixed
     */
    private function applyMap(mixed $value, array $map): mixed
    {
        if (is_array($value)) {
            return array_map(fn ($item) => $this->applyMap($item, $map), $value);
        }

        return is_string($value) && isset($map[$value]) ? $map[$value] : $value;
    }
}
