<?php

namespace App\Commands;

use App\Commands\Concerns\InteractsWithPijul;
use App\Prompts\LiveSuggestPrompt;
use Laravel\Prompts\MultiSelectPrompt;
use LaravelZero\Framework\Commands\Command;
use RuntimeException;

class PijulRecordWithIntentTagsCommand extends Command
{
    use InteractsWithPijul;

    private const PROMPT_SCROLL = 10;

    /**
     * @var string
     */
    protected $signature = 'pijul:record-with-intent-tags
                            {prefixes?* : Paths in which to record the changes}
                            {--a|all : Record all paths that have changed}
                            {--m|message= : Set the change message}
                            {--description= : Set the description field}
                            {--author= : Set the author field}
                            {--timestamp= : Set the timestamp field}
                            {--ignore-missing : Ignore missing (deleted) files}
                            {--working-copy= : Override the working copy path}
                            {--amend= : Amend this change instead of creating a new change}
                            {--identity= : Identity to sign changes with}
                            {--patience : Use Patience diff}
                            {--histogram : Use Histogram diff}
                            {--repository= : Work with the repository at PATH}
                            {--channel= : Work with CHANNEL instead of the current channel}
                            {--no-prompt : Abort rather than prompt for input}
                            {--list-tags : Print known intent tags and exit}
                            {--tag-limit=500 : Number of recent log entries to scan}';

    /**
     * @var string
     */
    protected $description = 'Record a Pijul change with reusable intent tags';

    /**
     * @var array<int, string>
     */
    protected $aliases = ['pijul-record-with-intent-tags'];

    public function handle(PijulListIntentTagsCommand $listIntentTags): int
    {
        $tags = $listIntentTags->resolveTags(
            limit: (string) $this->option('tag-limit'),
            repository: $this->option('repository'),
            channel: $this->option('channel'),
            noPrompt: (bool) $this->option('no-prompt'),
        );

        if ($this->option('list-tags')) {
            foreach ($tags as $tag) {
                $this->line($tag);
            }

            return self::SUCCESS;
        }

        $message = $this->option('message');

        if ($message !== null) {
            $this->printKnownTags($tags);

            $this->recordChange((string) $message);

            return self::SUCCESS;
        }

        if (! $this->input->isInteractive()) {
            $this->error('This command needs an interactive terminal unless you pass --message.');

            return self::FAILURE;
        }

        $this->printKnownTags($tags);

        $tag = $this->promptForTag($tags);
        $body = (string) $this->ask('message');
        $fullMessage = $this->buildMessage($tag, $body);

        if ($fullMessage === '') {
            $this->error('Change message cannot be empty.');

            return self::FAILURE;
        }

        $this->recordChange($fullMessage);

        return self::SUCCESS;
    }

    private function printKnownTags(array $tags): void
    {
        if ($tags === []) {
            return;
        }

        $this->line(sprintf('Known tags: %s', implode(', ', $tags)));
    }

    private function buildMessage(string $tag, string $body): string
    {
        $tag = trim($tag);
        $body = trim($body);

        if ($tag !== '' && $body !== '') {
            return sprintf('%s: %s', $tag, $body);
        }

        return $tag !== '' ? $tag : $body;
    }

    private function recordChange(string $message): void
    {
        if (! $this->input->isInteractive()) {
            $this->runPijul($this->recordArguments($message));

            return;
        }

        $hunks = $this->extractRecordHunks($this->captureRecordTemplate());
        $selectedHunks = $this->promptForHunkSelection($hunks);
        $editorScript = $this->createRecordEditorScript();

        try {
            $editorCommand = $this->recordEditorCommand($editorScript);

            $this->runPijul(
                $this->recordArguments(),
                environment: [
                    'EDITOR' => $editorCommand,
                    'VISUAL' => $editorCommand,
                    'ANI_PIJUL_EDITOR_MODE' => 'apply',
                    'ANI_PIJUL_MESSAGE' => $message,
                    'ANI_PIJUL_SELECTED_HUNKS' => json_encode($selectedHunks, JSON_THROW_ON_ERROR),
                ],
                disableTimeout: true,
            );
        } finally {
            unlink($editorScript);
        }
    }

    /**
     * @param  list<string>  $tags
     */
    private function promptForTag(array $tags): string
    {
        if ($tags === []) {
            return '';
        }

        return trim((string) (new LiveSuggestPrompt(
            label: 'Intent tag (optional)',
            options: fn (string $value): array => $this->suggestedTags($tags, $value),
            placeholder: 'Type to filter known tags, or press Enter to skip',
            scroll: self::PROMPT_SCROLL,
            hint: 'Results update as you type. Use arrows to choose, or press Enter on an empty prompt to skip.',
        ))->prompt());
    }

    private function captureRecordTemplate(): string
    {
        $editorScript = $this->createRecordEditorScript();
        $capturePath = tempnam(sys_get_temp_dir(), 'ani-pijul-capture-');

        if ($capturePath === false) {
            unlink($editorScript);

            throw new RuntimeException('Unable to create a temporary file for capturing Pijul hunks.');
        }

        try {
            $result = $this->tryPijul(
                $this->recordArguments(),
                environment: [
                    'EDITOR' => $this->recordEditorCommand($editorScript),
                    'VISUAL' => $this->recordEditorCommand($editorScript),
                    'ANI_PIJUL_EDITOR_MODE' => 'capture',
                    'ANI_PIJUL_CAPTURE_PATH' => $capturePath,
                ],
                disableTimeout: true,
            );

            $template = file_get_contents($capturePath);

            if (is_string($template) && $template !== '') {
                return $template;
            }

            $this->ensurePijulSucceeded($result);
        } finally {
            unlink($editorScript);

            if (is_file($capturePath)) {
                unlink($capturePath);
            }
        }

        throw new RuntimeException('Unable to capture Pijul hunks.');
    }

    /**
     * @return list<array{number:int,label:string}>
     */
    private function extractRecordHunks(string $template): array
    {
        preg_match_all('/(?ms)^(?<number>\d+)\.\s.*?(?=^\d+\.\s|\z)/', $template, $matches, PREG_SET_ORDER);

        return array_map(function (array $match): array {
            $lines = preg_split('/\R/', trim($match[0])) ?: [];
            $headline = trim((string) array_shift($lines));
            $detail = '';

            foreach ($lines as $line) {
                $line = trim($line);

                if ($line === '' || str_starts_with($line, 'up ')) {
                    continue;
                }

                $detail = $line;

                break;
            }

            return [
                'number' => (int) $match['number'],
                'label' => $detail === ''
                    ? $headline
                    : sprintf('%s - %s', $headline, $detail),
            ];
        }, $matches);
    }

    /**
     * @param  list<array{number:int,label:string}>  $hunks
     * @return list<int>
     */
    private function promptForHunkSelection(array $hunks): array
    {
        if ($hunks === []) {
            throw new RuntimeException('No Pijul hunks were found to record.');
        }

        $options = [];

        foreach ($hunks as $hunk) {
            $options[(string) $hunk['number']] = $hunk['label'];
        }

        return array_map(
            static fn (string $value): int => (int) $value,
            (new MultiSelectPrompt(
                label: 'Select hunks to record',
                options: $options,
                default: count($options) === 1 ? array_keys($options) : [],
                scroll: self::PROMPT_SCROLL,
                required: 'Select at least one hunk to record.',
                hint: 'Use space to toggle hunks, Ctrl+A to toggle all, and Enter to continue.',
            ))->prompt(),
        );
    }

    /**
     * @param  list<string>  $tags
     * @return list<string>
     */
    private function suggestedTags(array $tags, string $query): array
    {
        $query = trim($query);

        if ($query === '') {
            return array_values($tags);
        }

        return array_values(array_filter(
            $tags,
            fn (string $tag): bool => str_contains(strtolower($tag), strtolower($query)),
        ));
    }

    private function createRecordEditorScript(): string
    {
        $path = tempnam(sys_get_temp_dir(), 'ani-pijul-editor-');

        if ($path === false) {
            throw new RuntimeException('Unable to create a temporary Pijul editor helper.');
        }

        $script = <<<'PHP'
<?php

$file = $argv[1] ?? null;

if (! is_string($file) || $file === '') {
    fwrite(STDERR, "Missing Pijul editor file.\n");
    exit(1);
}

$mode = getenv('ANI_PIJUL_EDITOR_MODE');
$template = file_get_contents($file);

if ($template === false) {
    fwrite(STDERR, "Unable to read the Pijul editor file.\n");
    exit(1);
}

if ($mode === 'capture') {
    $capturePath = getenv('ANI_PIJUL_CAPTURE_PATH');

    if (! is_string($capturePath) || $capturePath === '') {
        fwrite(STDERR, "Missing ANI_PIJUL_CAPTURE_PATH.\n");
        exit(1);
    }

    if (file_put_contents($capturePath, $template) === false) {
        fwrite(STDERR, "Unable to capture the Pijul editor file.\n");
        exit(1);
    }

    fwrite(STDERR, "Captured Pijul hunks.\n");
    exit(1);
}

if ($mode !== 'apply') {
    fwrite(STDERR, "Unsupported ANI_PIJUL_EDITOR_MODE.\n");
    exit(1);
}

$message = getenv('ANI_PIJUL_MESSAGE');

if (! is_string($message)) {
    fwrite(STDERR, "Missing ANI_PIJUL_MESSAGE.\n");
    exit(1);
}

$encodedMessage = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

if (! is_string($encodedMessage)) {
    fwrite(STDERR, "Unable to encode the Pijul change message.\n");
    exit(1);
}

$updated = preg_replace('/^message = .*$/m', 'message = '.$encodedMessage, $template, 1, $count);

if (! is_string($updated) || $count !== 1) {
    fwrite(STDERR, "Unable to prefill the Pijul change message.\n");
    exit(1);
}

$selectedHunks = json_decode((string) getenv('ANI_PIJUL_SELECTED_HUNKS'), true);

if (! is_array($selectedHunks)) {
    fwrite(STDERR, "Missing ANI_PIJUL_SELECTED_HUNKS.\n");
    exit(1);
}

$selected = [];

foreach ($selectedHunks as $value) {
    if (is_int($value) || (is_string($value) && ctype_digit($value))) {
        $selected[(string) $value] = true;
    }
}

if ($selected === []) {
    fwrite(STDERR, "No hunks were selected.\n");
    exit(1);
}

if (preg_match('/^\d+\.\s/m', $updated, $firstHunk, PREG_OFFSET_CAPTURE) !== 1) {
    fwrite(STDERR, "Unable to find hunks in the Pijul editor file.\n");
    exit(1);
}

$header = rtrim(substr($updated, 0, $firstHunk[0][1]), "\n");
$body = substr($updated, $firstHunk[0][1]);

if (preg_match_all('/(?ms)^\d+\.\s.*?(?=^\d+\.\s|\z)/', $body, $matches) !== false) {
    $blocks = [];

    foreach ($matches[0] as $block) {
        if (preg_match('/^(?<number>\d+)\.\s/m', $block, $numberMatch) !== 1) {
            continue;
        }

        if (isset($selected[$numberMatch['number']])) {
            $blocks[] = rtrim($block, "\n");
        }
    }

    if ($blocks === []) {
        fwrite(STDERR, "None of the selected hunks matched the Pijul editor file.\n");
        exit(1);
    }

    $updated = $header."\n\n".implode("\n\n", $blocks)."\n";
}

if (file_put_contents($file, $updated) === false) {
    fwrite(STDERR, "Unable to update the Pijul editor file.\n");
    exit(1);
}
PHP;

        if (file_put_contents($path, $script) === false) {
            unlink($path);

            throw new RuntimeException('Unable to write the temporary Pijul editor helper.');
        }

        if (! chmod($path, 0700)) {
            unlink($path);

            throw new RuntimeException('Unable to make the temporary Pijul editor helper executable.');
        }

        return $path;
    }

    private function recordEditorCommand(string $script): string
    {
        return PHP_BINARY.' '.$script;
    }

    /**
     * @return array<int, string>
     */
    private function recordArguments(?string $message = null): array
    {
        $arguments = ['record'];

        if ($message !== null) {
            $arguments[] = '-m';
            $arguments[] = $message;
        }

        if ($this->option('all')) {
            $arguments[] = '--all';
        }

        foreach ([
            'description',
            'author',
            'timestamp',
            'working-copy',
            'identity',
            'repository',
            'channel',
        ] as $option) {
            $value = $this->option($option);

            if ($value !== null) {
                $arguments[] = sprintf('--%s', $option);
                $arguments[] = (string) $value;
            }
        }

        if ($this->input->hasParameterOption('--amend')) {
            $arguments[] = '--amend';

            if (($amend = $this->option('amend')) !== null) {
                $arguments[] = (string) $amend;
            }
        }

        foreach (['ignore-missing', 'patience', 'histogram', 'no-prompt'] as $option) {
            if ($this->option($option)) {
                $arguments[] = sprintf('--%s', $option);
            }
        }

        foreach ($this->argument('prefixes') as $prefix) {
            $arguments[] = (string) $prefix;
        }

        return $arguments;
    }
}