<?php

namespace App\Console\Commands;

use App\Models\ExportTarget;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;

/**
 * Empty a handover directory of folders nobody needs any more.
 *
 * Only where a destination asks for it — keep_days is null by default, because
 * a retention rule nobody configured is a deletion nobody expected.
 *
 * Safe in a way deleting usually is not: the invoice lives in the database as a
 * frozen document and its PDF is rendered from that on demand, so anything
 * removed here can be produced again and re-exported. The directory is a
 * handover point, not the record.
 *
 * Only ever removes folders whose NAME is a date it can parse and that are old
 * enough. Anything else in there — a stray file, a directory somebody made by
 * hand, a folder from a different layout — is left alone. A prune that deletes
 * what it does not recognise is how an archive loses something nobody was
 * watching.
 */
class PruneExportFolders extends Command
{
    protected $signature = 'clupilot:prune-exports {--dry-run}';

    protected $description = 'Remove dated folders older than a destination keeps them';

    public function handle(): int
    {
        $targets = ExportTarget::query()
            ->whereNotNull('keep_days')
            ->where('keep_days', '>', 0)
            // Only where this machine can see the files. An SFTP destination is
            // somebody else's disk with somebody else's retention on it.
            ->where('driver', ExportTarget::LOCAL)
            ->get();

        foreach ($targets as $target) {
            $this->prune($target);
        }

        return self::SUCCESS;
    }

    private function prune(ExportTarget $target): void
    {
        $root = rtrim($target->path, '/');

        if (! is_dir($root)) {
            // The mount is gone. Saying nothing and doing nothing is right:
            // this is a cleanup, and a cleanup must never be the thing that
            // reports a broken mount — nor act on whatever is underneath it.
            $this->warn("{$target->name}: path is not there, skipping ({$root})");

            return;
        }

        $cutoff = now()->subDays((int) $target->keep_days)->startOfDay();
        $removed = 0;

        foreach ((array) scandir($root) as $entry) {
            if ($entry === '.' || $entry === '..' || ! is_dir($root.'/'.$entry)) {
                continue;
            }

            $date = $this->dateFrom($entry);

            // Not a dated folder: left alone, deliberately. A prune that
            // deletes what it does not recognise is how an archive loses
            // something nobody was watching.
            if ($date === null || $date->gte($cutoff)) {
                continue;
            }

            if ($this->option('dry-run')) {
                $this->line("would remove {$root}/{$entry}");
                $removed++;

                continue;
            }

            $this->removeTree($root.'/'.$entry);
            $removed++;
        }

        $this->line("{$target->name}: {$removed} folder(s) older than {$target->keep_days} days.");
    }

    /**
     * Only the exact shapes this application writes.
     *
     * Matched with a pattern BEFORE parsing, not by asking Carbon and catching
     * what it does with nonsense: under strict mode createFromFormat throws
     * rather than returning false, so a folder somebody made by hand would end
     * the whole prune instead of being skipped. The pattern also says plainly
     * which two shapes are recognised, which is the thing a reader wants to
     * know here.
     */
    private function dateFrom(string $name): ?Carbon
    {
        if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $name, $m)) {
            return Carbon::createFromFormat('Y-m-d', $name)?->startOfDay();
        }

        if (preg_match('/^\d{4}$/', $name)) {
            return Carbon::createFromFormat('Y-m-d', $name.'-12-31')?->startOfDay();
        }

        return null;
    }

    private function removeTree(string $directory): void
    {
        foreach ((array) scandir($directory) as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $path = $directory.'/'.$entry;
            is_dir($path) ? $this->removeTree($path) : @unlink($path);
        }

        @rmdir($directory);
    }
}
