<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\Source\GitDiff;

use Butschster\ContextGenerator\Application\FSPath;
use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
use Butschster\ContextGenerator\Lib\Finder\FinderInterface;
use Butschster\ContextGenerator\Lib\Finder\FinderResult;
use Butschster\ContextGenerator\Lib\TreeBuilder\FileTreeBuilder;
use Butschster\ContextGenerator\Source\Fetcher\FilterableSourceInterface;
use Butschster\ContextGenerator\Source\GitDiff\Fetcher\CommitRangeParser;
use Butschster\ContextGenerator\Source\GitDiff\Fetcher\GitSourceFactory;
use Butschster\ContextGenerator\Source\GitDiff\Fetcher\GitSourceInterface;
use Psr\Log\LoggerInterface;
use Spiral\Files\FilesInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

/**
 * This finder extracts diffs from git commits, stashes, and other sources and applies filters to them
 */
final readonly class GitDiffFinder implements FinderInterface
{
    public function __construct(
        private FilesInterface $files,
        private GitSourceFactory $sourceFactory,
        private FileTreeBuilder $fileTreeBuilder,
        private CommitRangeParser $rangeParser,
        #[LoggerPrefix(prefix: 'git-diff-finder')]
        private ?LoggerInterface $logger = null,
    ) {}

    /**
     * Find git commit diffs based on the given source configuration
     *
     * @param FilterableSourceInterface $source Source configuration with filter criteria
     * @param string $basePath Optional base path to normalize file paths in the tree view
     * @return FinderResult The result containing found diffs and tree view
     */
    public function find(FilterableSourceInterface $source, string $basePath = '', array $options = []): FinderResult
    {
        if (!$source instanceof GitDiffSource) {
            throw new \InvalidArgumentException('Source must be an instance of CommitDiffSource');
        }

        // Get the commit range from the source
        $commitRange = $this->rangeParser->resolve($source->commit);

        $this->logger?->debug('Resolved commit range', [
            'original' => $source->commit,
            'resolved' => $commitRange,
        ]);

        // Get the appropriate Git source for this commit range
        $gitSource = $this->sourceFactory->create($commitRange);

        $this->logger?->debug('Selected Git source', [
            'source' => $gitSource::class,
        ]);

        // Create a temporary directory for the diffs
        $tempDir = FSPath::temp()->join('git-diff-' . \uniqid())->toString();
        $this->files->ensureDirectory($tempDir);

        try {
            // Get file infos from the Git source
            $this->logger?->debug('Getting file infos from Git source');
            $fileInfos = $gitSource->createFileInfos($source->repository, $commitRange, $tempDir);

            if (empty($fileInfos)) {
                $this->logger?->info('No changes found for the commit range', [
                    'commitRange' => $commitRange,
                ]);
                return new FinderResult([], 'No changes found');
            }

            $this->logger?->debug('Found files', [
                'count' => \count($fileInfos),
            ]);

            // Apply filters if needed
            $fileInfos = $this->applyFilters($fileInfos, $source, $tempDir);

            $this->logger?->debug('After applying filters', [
                'count' => \count($fileInfos),
            ]);

            // Get file paths for tree view
            $filePaths = \array_map(
                static fn(SplFileInfo $fileInfo)
                    => \method_exists($fileInfo, 'getOriginalPath')
                    ? $fileInfo->getOriginalPath()
                    : $fileInfo->getRelativePathname(),
                $fileInfos,
            );

            // Generate tree view
            $treeView = $this->generateTreeView($filePaths, $gitSource, $commitRange, $options);

            return new FinderResult(\array_values($fileInfos), $treeView);
        } catch (\Throwable $e) {
            $this->logger?->error('Error finding git diffs', [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            throw $e;
        } finally {
            $this->files->deleteDirectory($tempDir);
        }
    }

    /**
     * Apply filters to the file infos
     *
     * @param array<SplFileInfo> $fileInfos
     * @return array<SplFileInfo>
     */
    private function applyFilters(array $fileInfos, FilterableSourceInterface $source, string $tempDir): array
    {
        if (empty($fileInfos)) {
            return [];
        }

        $this->logger?->debug('Applying filters to file infos', [
            'fileCount' => \count($fileInfos),
            'name' => $source->name(),
            'path' => $source->path(),
            'notPath' => $source->notPath(),
            'contains' => $source->contains(),
            'notContains' => $source->notContains(),
        ]);

        $finder = new Finder();
        $finder->in($tempDir);

        // Apply name filter
        if ($source->name() !== null) {
            $finder->name($source->name());
        }

        // Apply path filter
        if ($source->path() !== null) {
            $finder->path($source->path());
        }

        // Apply notPath filter
        if ($source->notPath() !== null) {
            $finder->notPath($source->notPath());
        }

        // Apply contains filter
        if ($source->contains() !== null) {
            $finder->contains($source->contains());
        }

        // Apply notContains filter
        if ($source->notContains() !== null) {
            $finder->notContains($source->notContains());
        }

        // Get the filtered files
        $filteredPaths = [];
        foreach ($finder as $file) {
            $relativePath = FSPath::create($file->getPathname())->trim($tempDir)->toString();

            $filteredPaths[$relativePath] = true;
        }

        // Filter the file infos
        $filtered = \array_filter($fileInfos, static function (SplFileInfo $fileInfo) use ($filteredPaths, $tempDir) {
            $relativePath = FSPath::create($fileInfo->getPathname())->trim($tempDir)->toString();
            return isset($filteredPaths[$relativePath]);
        });

        $this->logger?->debug('Filter results', [
            'originalCount' => \count($fileInfos),
            'filteredCount' => \count($filtered),
        ]);

        return $filtered;
    }

    /**
     * Generate a tree view of the changed files
     *
     * @param array<string> $files List of file paths
     * @param GitSourceInterface $gitSource The Git source used for the commits
     * @param string|array $commitRange Commit range for the header
     * @return string Text representation of the file tree
     */
    private function generateTreeView(
        array $files,
        GitSourceInterface $gitSource,
        string|array $commitRange,
        array $options = [],
    ): string {
        if (empty($files)) {
            return "No changes found\n";
        }

        // Get a descriptive header from the Git source
        $treeHeader = $gitSource->formatReferenceForDisplay($commitRange) . "\n";

        // Build the tree
        $tree = $this->fileTreeBuilder->buildTree($files, '', $options);

        return $treeHeader . $tree;
    }
}
