<?php

declare(strict_types=1);

namespace App\Module\Integration\Infrastructure\Service;

use App\Module\Integration\Domain\Entity\ShareConfiguration;
use App\Module\Integration\Domain\Exception\ShareConnectionException;
use App\Module\Integration\Domain\Exception\ShareNotConfiguredException;
use App\Module\Integration\Domain\Repository\ShareConfigurationRepositoryInterface;
use App\Module\Integration\Domain\Service\FileEntry;
use App\Module\Integration\Domain\Service\FileSource;
use App\Module\Integration\Domain\Service\SharePathResolver;
use App\Module\Integration\Domain\Service\ShareTestResult;
use App\Shared\Infrastructure\Service\TokenEncryptor;
use Icewind\SMB\BasicAuth;
use Icewind\SMB\IFileInfo;
use Icewind\SMB\IShare;
use Icewind\SMB\ServerFactory;
use Symfony\Component\Mime\MimeTypes;
use Throwable;

use function count;

final class SmbFileSource implements FileSource
{
    /** Garde-fou : nombre maximum de dossiers explorés par recherche (évite de bloquer sur un très gros partage). */
    private const int SEARCH_MAX_DIRS = 2000;

    public function __construct(
        private readonly ShareConfigurationRepositoryInterface $configRepository,
        private readonly TokenEncryptor $tokenEncryptor,
        private readonly SharePathResolver $pathResolver,
    ) {}

    public function dir(string $relativePath): array
    {
        $config = $this->requireUsableConfig();
        $share  = $this->connect($config);
        $full   = $this->pathResolver->fullPath((string) $config->getBasePath(), $relativePath);

        try {
            $infos = $share->dir($full);
        } catch (Throwable $e) {
            throw new ShareConnectionException($e->getMessage(), 0, $e);
        }

        $entries = array_map(fn (IFileInfo $i): FileEntry => $this->toEntry($i, $relativePath), $infos);

        $this->sortEntries($entries);

        return $entries;
    }

    public function search(string $query, int $limit = 200): array
    {
        $needle = trim($query);

        if ('' === $needle) {
            return [];
        }

        $config = $this->requireUsableConfig();
        $share  = $this->connect($config);
        $base   = (string) $config->getBasePath();

        $results     = [];
        $queue       = ['']; // chemins relatifs des dossiers à explorer, racine en premier (parcours en largeur)
        $visitedDirs = 0;

        while ([] !== $queue && count($results) < $limit && $visitedDirs < self::SEARCH_MAX_DIRS) {
            $relative = array_shift($queue);
            $full     = $this->pathResolver->fullPath($base, $relative);

            try {
                $infos = $share->dir($full);
            } catch (Throwable) {
                continue; // dossier illisible (droits, lien mort…) : on l'ignore et on poursuit
            }
            ++$visitedDirs;

            foreach ($infos as $info) {
                $entry = $this->toEntry($info, $relative);

                if ($entry->isDir) {
                    $queue[] = $entry->path;
                }

                if (false !== mb_stripos($entry->name, $needle)) {
                    $results[] = $entry;

                    if (count($results) >= $limit) {
                        break;
                    }
                }
            }
        }

        $this->sortEntries($results);

        return $results;
    }

    public function read(string $relativePath)
    {
        $config = $this->requireUsableConfig();
        $share  = $this->connect($config);
        $full   = $this->pathResolver->fullPath((string) $config->getBasePath(), $relativePath);

        try {
            return $share->read($full);
        } catch (Throwable $e) {
            throw new ShareConnectionException($e->getMessage(), 0, $e);
        }
    }

    public function test(): ShareTestResult
    {
        try {
            $config = $this->requireUsableConfig();
            $share  = $this->connect($config);
            $share->dir($this->pathResolver->fullPath((string) $config->getBasePath(), ''));

            return new ShareTestResult(true);
        } catch (ShareNotConfiguredException $e) {
            return new ShareTestResult(false, 'Configuration incomplète ou désactivée.');
        } catch (Throwable $e) {
            return new ShareTestResult(false, $e->getMessage());
        }
    }

    private function requireUsableConfig(): ShareConfiguration
    {
        $config = $this->configRepository->findSingleton();

        if (null === $config || !$config->isUsable()) {
            throw new ShareNotConfiguredException('Share is not configured or disabled.');
        }

        return $config;
    }

    private function connect(ShareConfiguration $config): IShare
    {
        $password = null !== $config->getEncryptedPassword()
            ? $this->tokenEncryptor->decrypt($config->getEncryptedPassword())
            : '';

        $auth = new BasicAuth(
            (string) $config->getUsername(),
            $config->getDomain() ?: 'WORKGROUP',
            $password,
        );
        $server = new ServerFactory()->createServer((string) $config->getHost(), $auth);

        try {
            return $server->getShare((string) $config->getShareName());
        } catch (Throwable $e) {
            throw new ShareConnectionException($e->getMessage(), 0, $e);
        }
    }

    /**
     * Trie en place : dossiers d'abord, puis tri alphabétique insensible à la casse.
     *
     * @param FileEntry[] $entries
     */
    private function sortEntries(array &$entries): void
    {
        usort($entries, static function (FileEntry $a, FileEntry $b): int {
            if ($a->isDir !== $b->isDir) {
                return $a->isDir ? -1 : 1;
            }

            return strcasecmp($a->name, $b->name);
        });
    }

    private function toEntry(IFileInfo $info, string $parentRelative): FileEntry
    {
        $parent = '' === $parentRelative ? '' : rtrim($parentRelative, '/').'/';
        $path   = $parent.$info->getName();
        $isDir  = $info->isDirectory();

        $mime = 'application/octet-stream';
        if (!$isDir) {
            $guessed = MimeTypes::getDefault()->getMimeTypes(pathinfo($info->getName(), PATHINFO_EXTENSION));
            $mime    = $guessed[0] ?? 'application/octet-stream';
        }

        return new FileEntry(
            name: $info->getName(),
            path: $path,
            isDir: $isDir,
            size: $isDir ? 0 : $info->getSize(),
            modifiedAt: $info->getMTime(),
            mimeType: $mime,
        );
    }
}
