<?php

namespace App\Services;

use App\Services\Storage\FileStorageException;
use Aws\Exception\AwsException;
use Aws\S3\S3Client;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Storage;

class StorageBrowserService
{
    private Storage $config;

    private FileStorageService $storage;

    /** @var list<string> */
    private array $folders = [
        'uploads/policy/policy_pdf',
        'uploads/policy/policy_payment_receipt',
        'uploads/policy/policy_md',
        'uploads/endorsement',
        'uploads/endorsement/endorsement_pdf',
        'uploads/agent/certificate_file',
        'uploads/agent/incentive_file',
        'uploads/enquiry/id_proof',
        'uploads/enquiry/rc',
        'uploads/enquiry/previous_policy',
        'uploads/enquiry',
        'uploads/quotation',
        'uploads/claims',
    ];

    /** @var list<string> */
    private array $skipFiles = ['index.html', '.htaccess', '.gitkeep'];

    public function __construct(?Storage $config = null, ?FileStorageService $storage = null)
    {
        $this->config  = $config ?? config('Storage');
        $this->storage = $storage ?? service('fileStorage');
    }

    public function driverLabel(): string
    {
        return $this->storage->isS3() ? 's3' : 'local';
    }

    public function bucketLabel(): string
    {
        if ($this->config->s3Bucket !== '') {
            return $this->config->s3Prefix !== ''
                ? $this->config->s3Bucket . '/' . $this->config->s3Prefix
                : $this->config->s3Bucket;
        }

        return $this->storage->isS3() ? 's3' : 'local-storage';
    }

    /**
     * @return list<array{value: string, label: string}>
     */
    public function folders(): array
    {
        $items = [];

        foreach ($this->folders as $folder) {
            $items[] = [
                'value' => $folder,
                'label' => $folder,
            ];
        }

        return $items;
    }

    /**
     * @return array{
     *     files: list<array{key: string, file_name: string, size: int, size_human: string, last_modified: string}>,
     *     next_token: ?string,
     *     has_more: bool
     * }
     */
    public function listFiles(string $folder, int $perPage = 10, ?string $continuationToken = null, ?string $search = null): array
    {
        $folder = $this->normalizeFolder($folder);
        $search = $search !== null ? trim($search) : '';

        if ($this->storage->isS3()) {
            return $this->listS3Files($folder, $perPage, $continuationToken, $search);
        }

        return $this->listLocalFiles($folder, $perPage, $continuationToken, $search);
    }

    public function assertAllowedKey(string $key): void
    {
        $key = ltrim(str_replace('\\', '/', $key), '/');

        if ($key === '' || str_contains($key, '..')) {
            throw new FileStorageException('Invalid file key.');
        }

        if (! str_starts_with($key, 'uploads/')) {
            throw new FileStorageException('Access denied for this path.');
        }
    }

    public function getOpenUrl(string $key): string
    {
        $this->assertAllowedKey($key);

        if (! $this->storage->existsKey($key)) {
            throw new FileStorageException('File not found.');
        }

        if ($this->storage->isS3()) {
            return $this->storage->getTemporaryUrlForKey($key);
        }

        return site_url('storage/browser/download?key=' . rawurlencode($key));
    }

    public function downloadResponse(string $key): ResponseInterface
    {
        $this->assertAllowedKey($key);

        return $this->storage->downloadKey($key, basename($key));
    }

    private function normalizeFolder(string $folder): string
    {
        $folder = trim(str_replace('\\', '/', $folder), '/');

        if ($folder === '' || str_contains($folder, '..')) {
            throw new FileStorageException('Invalid folder.');
        }

        if (! in_array($folder, $this->folders, true)) {
            throw new FileStorageException('Folder is not allowed.');
        }

        return $folder;
    }

    /**
     * @return array{
     *     files: list<array{key: string, file_name: string, size: int, size_human: string, last_modified: string}>,
     *     next_token: ?string,
     *     has_more: bool
     * }
     */
    private function listLocalFiles(string $folder, int $perPage, ?string $continuationToken, string $search): array
    {
        $dir    = rtrim($this->config->localRoot, '/\\')
            . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folder);

        if (! is_dir($dir)) {
            return ['files' => [], 'next_token' => null, 'has_more' => false];
        }

        $offset = 0;
        if ($continuationToken !== null && $continuationToken !== '') {
            $decoded = json_decode(base64_decode($continuationToken, true) ?: '', true);
            $offset  = is_array($decoded) ? max(0, (int) ($decoded['offset'] ?? 0)) : 0;
        }

        $entries = [];
        $handle  = opendir($dir);

        if ($handle === false) {
            throw new FileStorageException('Unable to read folder.');
        }

        while (($entry = readdir($handle)) !== false) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            if (in_array(strtolower($entry), $this->skipFiles, true)) {
                continue;
            }

            $fullPath = $dir . DIRECTORY_SEPARATOR . $entry;

            if (! is_file($fullPath)) {
                continue;
            }

            if ($search !== '' && stripos($entry, $search) === false) {
                continue;
            }

            $entries[] = [
                'key'           => $folder . '/' . $entry,
                'file_name'     => $entry,
                'size'          => (int) (filesize($fullPath) ?: 0),
                'size_human'    => $this->formatBytes((int) (filesize($fullPath) ?: 0)),
                'last_modified' => date('Y-m-d H:i:s', (int) filemtime($fullPath)),
            ];
        }

        closedir($handle);

        usort($entries, static fn (array $a, array $b): int => strcmp($b['last_modified'], $a['last_modified']));

        $slice   = array_slice($entries, $offset, $perPage);
        $next    = $offset + $perPage;
        $hasMore = $next < count($entries);

        return [
            'files'      => $slice,
            'next_token' => $hasMore ? base64_encode(json_encode(['offset' => $next], JSON_THROW_ON_ERROR)) : null,
            'has_more'   => $hasMore,
        ];
    }

    /**
     * @return array{
     *     files: list<array{key: string, file_name: string, size: int, size_human: string, last_modified: string}>,
     *     next_token: ?string,
     *     has_more: bool
     * }
     */
    private function listS3Files(string $folder, int $perPage, ?string $continuationToken, string $search): array
    {
        if ($this->config->s3Bucket === '' || $this->config->s3AccessKey === '' || $this->config->s3SecretKey === '') {
            throw new FileStorageException('S3 is not configured.');
        }

        $client = new S3Client([
            'version'     => 'latest',
            'region'      => $this->config->s3Region,
            'credentials' => [
                'key'    => $this->config->s3AccessKey,
                'secret' => $this->config->s3SecretKey,
            ],
        ]);

        $prefix = $this->objectKey($folder . '/');
        $files  = [];
        $token  = $continuationToken ?: null;
        $hasMore = false;

        try {
            while (count($files) < $perPage) {
                $params = [
                    'Bucket'  => $this->config->s3Bucket,
                    'Prefix'  => $prefix,
                    'MaxKeys' => max($perPage * 3, 30),
                ];

                if ($token !== null) {
                    $params['ContinuationToken'] = $token;
                }

                $result = $client->listObjectsV2($params);
                $items  = $result['Contents'] ?? [];

                foreach ($items as $item) {
                    $s3Key = (string) ($item['Key'] ?? '');

                    if ($s3Key === '' || str_ends_with($s3Key, '/')) {
                        continue;
                    }

                    $logicalKey = $this->stripObjectPrefix($s3Key);
                    $fileName   = basename($logicalKey);

                    if (in_array(strtolower($fileName), $this->skipFiles, true)) {
                        continue;
                    }

                    if ($search !== '' && stripos($fileName, $search) === false) {
                        continue;
                    }

                    $files[] = [
                        'key'           => $logicalKey,
                        'file_name'     => $fileName,
                        'size'          => (int) ($item['Size'] ?? 0),
                        'size_human'    => $this->formatBytes((int) ($item['Size'] ?? 0)),
                        'last_modified' => isset($item['LastModified'])
                            ? $item['LastModified']->format('Y-m-d H:i:s')
                            : '',
                    ];

                    if (count($files) >= $perPage) {
                        break;
                    }
                }

                $token   = $result['IsTruncated'] ? ($result['NextContinuationToken'] ?? null) : null;
                $hasMore = $token !== null;

                if ($token === null) {
                    break;
                }

                if (count($files) >= $perPage) {
                    break;
                }
            }
        } catch (AwsException $e) {
            throw new FileStorageException('S3 list failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }

        return [
            'files'      => $files,
            'next_token' => $hasMore ? $token : null,
            'has_more'   => $hasMore,
        ];
    }

    private function objectKey(string $key): string
    {
        $key = ltrim(str_replace('\\', '/', $key), '/');

        return $this->config->s3Prefix !== ''
            ? $this->config->s3Prefix . '/' . $key
            : $key;
    }

    private function stripObjectPrefix(string $s3Key): string
    {
        if ($this->config->s3Prefix === '') {
            return $s3Key;
        }

        $prefix = $this->config->s3Prefix . '/';

        return str_starts_with($s3Key, $prefix)
            ? substr($s3Key, strlen($prefix))
            : $s3Key;
    }

    private function formatBytes(int $bytes): string
    {
        if ($bytes < 1024) {
            return $bytes . ' B';
        }

        if ($bytes < 1048576) {
            return round($bytes / 1024, 1) . ' KB';
        }

        return round($bytes / 1048576, 1) . ' MB';
    }
}
