<?php

declare(strict_types=1);

namespace Flowaxy\Infrastructure\Filesystem;

use Flowaxy\Contracts\Filesystem\FileInterface;
use Flowaxy\Support\Facades\Log;
use Exception;

use function array_pop;
use function basename;
use function chmod;
use function copy;
use function dirname;
use function error_reporting;
use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function filectime;
use function filemtime;
use function filesize;
use function finfo_file;
use function finfo_open;
use function function_exists;
use function implode;
use function is_file;
use function is_readable;
use function is_writable;
use function json_encode;
use function mkdir;
use function mime_content_type;
use function pathinfo;
use function preg_match;
use function preg_replace;
use function realpath;
use function rename;
use function str_replace;
use function str_starts_with;
use function strtolower;
use function strlen;
use function unlink;
use const DS;
use const FILEINFO_MIME_TYPE;
use const FILE_APPEND;
use const LOCK_EX;
use const PATHINFO_BASENAME;
use const PATHINFO_EXTENSION;
use const PATHINFO_FILENAME;

// Клас для роботи з файлами
// Загальні операції з файлами: читання, запис, копіювання, видалення, перейменування
final class File implements FileInterface
{
    private string $filePath;

    // Конструктор
    public function __construct(?string $filePath = null)
    {
        if ($filePath !== null) {
            $this->filePath = $filePath;
        }
    }

    // Встановлення шляху до файла
    public function setPath(string $filePath): self
    {
        $this->filePath = $filePath;

        return $this;
    }

    // Отримання шляху до файла
    public function getPath(): string
    {
        return $this->filePath;
    }

    // Перевірка існування файла
    public function exists(): bool
    {
        return file_exists($this->filePath) && is_file($this->filePath);
    }

    // Читання вмісту файла
    public function read(): string
    {
        Log::debug('File::read: Reading file', ['file' => $this->filePath]);

        if (! $this->exists()) {
            Log::error('File::read: File does not exist', ['file' => $this->filePath]);
            throw new \Exception("Файл не існує: {$this->filePath}");
        }

        if (! is_readable($this->filePath)) {
            Log::error('File::read: File is not readable', ['file' => $this->filePath]);
            throw new \Exception("Файл недоступний для читання: {$this->filePath}");
        }

        $content = @file_get_contents($this->filePath);

        if ($content === false) {
            Log::error('File::read: Failed to read file', ['file' => $this->filePath]);
            throw new \Exception("Не вдалося прочитати файл: {$this->filePath}");
        }

        Log::info('File::read: File read successfully', [
            'file' => $this->filePath,
            'size' => strlen($content),
        ]);

        return $content;
    }

    // Запис вмісту в файл
    public function write(string $content, bool $append = false): bool
    {
        Log::debug('File::write: Writing file', [
            'file' => $this->filePath,
            'append' => $append,
            'size' => strlen($content),
        ]);

        $this->ensureDirectory();

        $flags = $append ? FILE_APPEND | LOCK_EX : LOCK_EX;
        $result = @file_put_contents($this->filePath, $content, $flags);

        if ($result === false) {
            Log::error('File::write: Failed to write file', ['file' => $this->filePath]);
            throw new \Exception("Не вдалося записати файл: {$this->filePath}");
        }

        // Пытаемся установить права доступа, но не критично, если не получится
        $this->setPermissions(0644);

        Log::info('File::write: File written successfully', [
            'file' => $this->filePath,
            'append' => $append,
            'bytes_written' => $result,
        ]);

        return true;
    }

    // Копіювання файла
    public function copy(string $destinationPath): bool
    {
        if (function_exists('logDebug')) {
            logDebug('File::copy: Copying file', [
                'source' => $this->filePath,
                'destination' => $destinationPath,
            ]);
        }

        if (! $this->exists()) {
                Log::error('File::copy: Source file does not exist', ['file' => $this->filePath]);
            throw new \Exception("Вихідний файл не існує: {$this->filePath}");
        }

        $this->ensureDirectory($destinationPath);

        if (! @copy($this->filePath, $destinationPath)) {
            if (function_exists('logError')) {
                logError('File::copy: Failed to copy file', [
                    'source' => $this->filePath,
                    'destination' => $destinationPath,
                ]);
            }
            throw new \Exception("Не вдалося скопіювати файл з '{$this->filePath}' в '{$destinationPath}'");
        }

        // Пытаемся установить права доступа, но не критично, если не получится
        $this->setPermissionsOnPath($destinationPath, 0644);

        if (function_exists('logInfo')) {
            logInfo('File::copy: File copied successfully', [
                'source' => $this->filePath,
                'destination' => $destinationPath,
            ]);
        }

        return true;
    }

    // Переміщення/перейменування файла
    public function move(string $destinationPath): bool
    {
        if (function_exists('logDebug')) {
            logDebug('File::move: Moving file', [
                'source' => $this->filePath,
                'destination' => $destinationPath,
            ]);
        }

        if (! $this->exists()) {
                Log::error('File::move: Source file does not exist', ['file' => $this->filePath]);
            throw new \Exception("Вихідний файл не існує: {$this->filePath}");
        }

        $this->ensureDirectory($destinationPath);

        if (! @rename($this->filePath, $destinationPath)) {
            if (function_exists('logError')) {
                logError('File::move: Failed to move file', [
                    'source' => $this->filePath,
                    'destination' => $destinationPath,
                ]);
            }
            throw new \Exception("Не вдалося перемістити файл з '{$this->filePath}' в '{$destinationPath}'");
        }

        $this->filePath = $destinationPath;

        if (function_exists('logInfo')) {
            logInfo('File::move: File moved successfully', [
                'source' => $this->filePath,
                'destination' => $destinationPath,
            ]);
        }

        return true;
    }

    // Видалення файла
    public function delete(): bool
    {
        Log::debug('File::delete: Deleting file', ['file' => $this->filePath]);

        if (! $this->exists()) {
            Log::debug('File::delete: File does not exist', ['file' => $this->filePath]);
            return true;
        }

        if (! @unlink($this->filePath)) {
                Log::error('File::delete: Failed to delete file', ['file' => $this->filePath]);
            throw new \Exception("Не вдалося видалити файл: {$this->filePath}");
        }

        Log::info('File::delete: File deleted successfully', ['file' => $this->filePath]);

        return true;
    }

    // Отримання розміру файла
    public function getSize(): int
    {
        return $this->exists() ? filesize($this->filePath) : 0;
    }

    // Отримання MIME типу файла
    public function getMimeType(): string|false
    {
        if (! $this->exists()) {
            return false;
        }

        if (function_exists('mime_content_type')) {
            return @mime_content_type($this->filePath);
        }

        if (function_exists('finfo_file')) {
            $finfo = @finfo_open(FILEINFO_MIME_TYPE);
            if ($finfo === false) {
                return false;
            }

            $mimeType = @finfo_file($finfo, $this->filePath);

            // finfo_close() is deprecated in PHP 8.1+, resource is automatically closed
            return $mimeType;
        }

        return false;
    }

    // Отримання часу останньої зміни
    public function getMTime(): int|false
    {
        return $this->exists() ? @filemtime($this->filePath) : false;
    }

    // Отримання часу створення
    public function getCTime()
    {
        return $this->exists() ? @filectime($this->filePath) : false;
    }

    // Отримання розширення файла
    public function getExtension(): string
    {
        return strtolower(pathinfo($this->filePath, PATHINFO_EXTENSION));
    }

    // Отримання імені файла з розширенням
    public function getBasename(): string
    {
        return pathinfo($this->filePath, PATHINFO_BASENAME);
    }

    // Отримання імені файла без шляху та розширення
    public function getFilename(): string
    {
        return pathinfo($this->filePath, PATHINFO_FILENAME);
    }

    // Отримання директорії файла
    public function getDirectory(): string
    {
        return dirname($this->filePath);
    }

    // Перевірка, чи є файл читабельним
    public function isReadable(): bool
    {
        return $this->exists() && is_readable($this->filePath);
    }

    // Перевірка, чи є файл доступним для запису
    public function isWritable(): bool
    {
        return $this->exists() && is_writable($this->filePath);
    }

    // Встановлення прав доступу до файла
    public function chmod(int $mode): bool
    {
        if (! $this->exists()) {
            return false;
        }

        // Тиха установка прав доступа без логирования ошибок
        $oldErrorReporting = error_reporting(0);
        $result = @chmod($this->filePath, $mode);
        error_reporting($oldErrorReporting);

        return $result !== false;
    }

    // Створення порожнього файла
    public function create(): bool
    {
        return $this->exists() ? true : $this->write('');
    }

    // Додавання вмісту в кінець файла
    public function append(string $content): bool
    {
        return $this->write($content, true);
    }

    // Тиха установка прав доступа без логирования ошибок
    private function setPermissions(int $mode): void
    {
        if (! $this->exists()) {
            return;
        }

        // Пытаемся установить права, но игнорируем ошибки
        // На некоторых системах (Windows, WSL) chmod может не работать
        $oldErrorReporting = error_reporting(0);
        @chmod($this->filePath, $mode);
        error_reporting($oldErrorReporting);
    }

    // Тиха установка прав доступа для указанного пути
    private function setPermissionsOnPath(string $path, int $mode): void
    {
        if (! file_exists($path)) {
            return;
        }

        // Пытаемся установить права, но игнорируем ошибки
        // На некоторых системах (Windows, WSL) chmod может не работать
        $oldErrorReporting = error_reporting(0);
        @chmod($path, $mode);
        error_reporting($oldErrorReporting);
    }

    // Переконатися, що директорія існує
    private function ensureDirectory(?string $filePath = null): void
    {
        $dir = dirname($filePath ?? $this->filePath);

        if (! is_dir($dir) && ! @mkdir($dir, 0755, true)) {
            throw new \Exception("Не вдалося створити директорію: {$dir}");
        }
    }

    // Статичний метод: Перевірка існування файла
    // Перейменовано з exists() щоб уникнути конфлікту з методом екземпляра
    public static function fileExists(string $filePath): bool
    {
        return file_exists($filePath) && is_file($filePath);
    }

    // Статичний метод: Читання файла
    // Перейменовано з read() щоб уникнути конфлікту з методом екземпляра
    public static function readFile(string $filePath)
    {
        try {
            return (new self($filePath))->read();
        } catch (\Exception $e) {
            Log::error('File::readFile error: ' . $e->getMessage(), ['exception' => $e, 'file' => $filePath]);
            return false;
        }
    }

    // Перевірка, чи знаходиться файл всередині базової директорії (захист від path traversal)
    public function isPathSafe(string $basePath): bool
    {
        $realBasePath = realpath($basePath);
        $realFilePath = realpath($this->filePath);

        if ($realBasePath === false || $realFilePath === false) {
            // Якщо файл не існує, перевіряємо директорію
            $realDirPath = realpath(dirname($this->filePath));
            if ($realDirPath === false) {
                return false;
            }
            $realFilePath = $realDirPath;
        }

        // Нормалізуємо шляхи для порівняння (замінюємо зворотні слеші на прямі)
        $normalizedBasePath = str_replace('\\', '/', $realBasePath);
        $normalizedFilePath = str_replace('\\', '/', $realFilePath);

        return str_starts_with($normalizedFilePath, $normalizedBasePath);
    }

    // Нормалізація шляху (видалення .., . та дублюючих слешів)
    public static function normalizePath(string $path): string
    {
        // Замінюємо зворотні слеші на прямі
        $path = str_replace('\\', '/', $path);

        // Видаляємо дублюючі слеші
        $path = preg_replace('#/+#', '/', $path);

        // Обробка ..
        $parts = explode('/', $path);
        $result = [];

        foreach ($parts as $part) {
            if ($part === '' || $part === '.') {
                continue;
            }
            if ($part === '..') {
                if (! empty($result)) {
                    array_pop($result);
                }
            } else {
                $result[] = $part;
            }
        }

        return implode('/', $result);
    }

    // Статичний метод: Перевірка, чи знаходиться шлях всередині базової директорії
    public static function isPathInDirectory(string $path, string $basePath): bool
    {
        $file = new self($path);

        return $file->isPathSafe($basePath);
    }

    // Статичний метод: Запис в файл
    // Перейменовано з write() щоб уникнути конфлікту з методом екземпляра
    public static function writeFile(string $filePath, string $content, bool $append = false): bool
    {
        try {
            return (new self($filePath))->write($content, $append);
        } catch (\Exception $e) {
            Log::error('File::writeFile error: ' . $e->getMessage(), ['exception' => $e, 'file' => $filePath]);
            return false;
        }
    }

    // Статичний метод: Копіювання файла
    // Перейменовано з copy() щоб уникнути конфлікту з методом екземпляра
    public static function copyFile(string $sourcePath, string $destinationPath): bool
    {
        try {
            return (new self($sourcePath))->copy($destinationPath);
        } catch (\Exception $e) {
            Log::error('File::copyFile error: ' . $e->getMessage(), ['exception' => $e, 'source' => $sourcePath, 'destination' => $destinationPath]);
            return false;
        }
    }

    // Статичний метод: Видалення файла
    // Перейменовано з delete() щоб уникнути конфлікту з методом екземпляра
    public static function deleteFile(string $filePath): bool
    {
        try {
            return (new self($filePath))->delete();
        } catch (\Exception $e) {
            Log::error('File::deleteFile error: ' . $e->getMessage(), ['exception' => $e, 'file' => $filePath]);
            return false;
        }
    }

    // Статичний метод: Отримання розміру файла
    public static function size(string $filePath): int
    {
        return (new self($filePath))->getSize();
    }

    // Статичний метод: Отримання MIME типу
    public static function mimeType(string $filePath)
    {
        return (new self($filePath))->getMimeType();
    }
}
