<?php

declare(strict_types=1);

namespace App\Domain\Files\Services;

use App\Domain\Account\Models\Account;
use App\Domain\Sysapi\Facades\Sysapi;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use RuntimeException;

/**
 * Per-account file operations (Phase 1 Epic 10).
 *
 * Production'da Laravel Storage'ı per-account disk olarak yapılandırıyoruz:
 *   - Dev: storage/app/customer-files/{username}/
 *   - Production: /home/{username}/ (sysapi tarafından chown'lu)
 *
 * Tüm path'ler relative — leading slash strip edilir, '../' reddedilir.
 */
final class FileService
{
    public function disk(Account $account): Filesystem
    {
        // Dev'de local storage altında izole klasör
        if (app()->environment('local', 'testing')) {
            $root = storage_path('app/customer-files/'.$account->username);
            if (! is_dir($root)) {
                mkdir($root, 0755, true);
            }

            return Storage::build([
                'driver' => 'local',
                'root' => $root,
            ]);
        }

        // Production: gerçek customer home (sysapi tarafından chown'lu)
        return Storage::build([
            'driver' => 'local',
            'root' => $account->homeDirectory(),
        ]);
    }

    private function safe(string $path): string
    {
        $path = ltrim($path, '/\\');

        // Path traversal koruması
        if (str_contains($path, '..') || str_contains($path, "\0")) {
            throw new RuntimeException('Geçersiz path.');
        }

        return $path;
    }

    public function list(Account $account, string $path = ''): array
    {
        $path = $this->safe($path);
        $disk = $this->disk($account);

        if ($path !== '' && ! $disk->directoryExists($path)) {
            throw new RuntimeException('Klasör bulunamadı.');
        }

        $files = collect($disk->files($path))->map(fn ($f) => [
            'name' => basename($f),
            'path' => $f,
            'type' => 'file',
            'size' => $disk->size($f),
            'mime' => $disk->mimeType($f) ?: 'application/octet-stream',
            'modified' => $disk->lastModified($f),
            'permissions' => $this->filePermissions($disk->path($f)),
        ]);

        $dirs = collect($disk->directories($path))->map(fn ($d) => [
            'name' => basename($d),
            'path' => $d,
            'type' => 'dir',
            'size' => 0,
            'mime' => 'directory',
            'modified' => null,
            'permissions' => $this->filePermissions($disk->path($d)),
        ]);

        return $dirs->concat($files)->values()->all();
    }

    public function upload(Account $account, string $path, UploadedFile $file): string
    {
        $path = $this->safe($path);
        $disk = $this->disk($account);

        $name = $file->getClientOriginalName();
        $target = $path === '' ? $name : $path.'/'.$name;

        $disk->putFileAs($path, $file, $name);

        return $target;
    }

    public function mkdir(Account $account, string $path): void
    {
        $path = $this->safe($path);
        $disk = $this->disk($account);
        $disk->makeDirectory($path);
    }

    public function delete(Account $account, string $path): void
    {
        $path = $this->safe($path);
        if ($path === '') {
            throw new RuntimeException('Root silinemez.');
        }

        $disk = $this->disk($account);

        if ($disk->directoryExists($path)) {
            $disk->deleteDirectory($path);
        } else {
            $disk->delete($path);
        }
    }

    public function rename(Account $account, string $oldPath, string $newPath): void
    {
        $oldPath = $this->safe($oldPath);
        $newPath = $this->safe($newPath);

        $disk = $this->disk($account);
        $disk->move($oldPath, $newPath);
    }

    public function download(Account $account, string $path): string
    {
        $path = $this->safe($path);
        $disk = $this->disk($account);

        if (! $disk->exists($path)) {
            throw new RuntimeException('Dosya bulunamadı.');
        }

        return $disk->path($path);
    }

    /**
     * Dosya içeriğini metin olarak oku (editor için).
     */
    public function read(Account $account, string $path, int $maxBytes = 2_097_152): string
    {
        $path = $this->safe($path);
        $disk = $this->disk($account);

        if (! $disk->exists($path)) {
            throw new RuntimeException('Dosya bulunamadı.');
        }

        $size = $disk->size($path);
        if ($size > $maxBytes) {
            throw new RuntimeException('Dosya çok büyük (>'.number_format($maxBytes / 1024 / 1024, 1).' MB) — editörde açılamaz, SSH/SFTP kullanın.');
        }

        return (string) $disk->get($path);
    }

    public function save(Account $account, string $path, string $content): void
    {
        $path = $this->safe($path);
        $disk = $this->disk($account);

        if (! $disk->exists($path)) {
            throw new RuntimeException('Dosya bulunamadı.');
        }

        $disk->put($path, $content);
    }

    /**
     * Dosya/klasörü zip arşivine paketle.
     */
    public function zip(Account $account, string $sourcePath, string $zipName): string
    {
        $sourcePath = $this->safe($sourcePath);
        $zipName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $zipName);
        if (! str_ends_with($zipName, '.zip')) {
            $zipName .= '.zip';
        }

        $disk = $this->disk($account);
        if (! $disk->exists($sourcePath) && ! $disk->directoryExists($sourcePath)) {
            throw new RuntimeException('Kaynak bulunamadı.');
        }

        $absSource = $disk->path($sourcePath);
        $parent = dirname($sourcePath);
        $zipPath = ($parent === '.' || $parent === '') ? $zipName : $parent.'/'.$zipName;
        $absZip = $disk->path($zipPath);

        if (! class_exists(\ZipArchive::class)) {
            throw new RuntimeException('ZipArchive PHP eklentisi yüklü değil.');
        }

        $zip = new \ZipArchive;
        if ($zip->open($absZip, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
            throw new RuntimeException('Zip oluşturulamadı.');
        }

        if (is_dir($absSource)) {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($absSource, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::LEAVES_ONLY,
            );
            $baseLen = strlen(rtrim($absSource, '/\\')) + 1;
            foreach ($iterator as $file) {
                if ($file->isFile()) {
                    $relative = substr($file->getPathname(), $baseLen);
                    $zip->addFile($file->getPathname(), $relative);
                }
            }
        } else {
            $zip->addFile($absSource, basename($absSource));
        }
        $zip->close();

        return $zipPath;
    }

    /**
     * Zip arşivini bulunduğu klasöre çıkar.
     */
    public function unzip(Account $account, string $zipPath, int $maxBytes = 524_288_000): string
    {
        $zipPath = $this->safe($zipPath);
        $disk = $this->disk($account);

        if (! $disk->exists($zipPath)) {
            throw new RuntimeException('Zip dosyası bulunamadı.');
        }

        if ($disk->size($zipPath) > $maxBytes) {
            throw new RuntimeException('Zip çok büyük (>'.number_format($maxBytes / 1024 / 1024, 0).' MB).');
        }

        if (! class_exists(\ZipArchive::class)) {
            throw new RuntimeException('ZipArchive PHP eklentisi yüklü değil.');
        }

        $abs = $disk->path($zipPath);
        $targetDir = dirname($abs);

        $zip = new \ZipArchive;
        if ($zip->open($abs) !== true) {
            throw new RuntimeException('Zip açılamadı (bozuk veya parola korumalı).');
        }

        // Zip slip koruması — root dışına yazıp yazmadığını kontrol
        $rootAbs = realpath($disk->path(''));
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entry = $zip->getNameIndex($i);
            if (str_contains($entry, '..') || str_starts_with($entry, '/')) {
                $zip->close();
                throw new RuntimeException('Zip içinde güvensiz path (zip slip).');
            }
        }

        $zip->extractTo($targetDir);
        $zip->close();

        return $targetDir;
    }

    /**
     * Dosya/klasör izinlerini değiştir.
     *
     * @param  array<string>  $paths  Relative paths under account home
     * @param  string  $mode  3 or 4 digit octal e.g. "755" or "0755"
     */
    public function chmod(Account $account, array $paths, string $mode, bool $recursive = false): void
    {
        if (! preg_match('/^0?[0-7]{3}$/', $mode)) {
            throw new RuntimeException('Geçersiz mod: octal 3 veya 4 basamak gerekli (örn: 755, 0755).');
        }

        $disk = $this->disk($account);
        $safePaths = [];
        foreach ($paths as $p) {
            $safe = $this->safe($p);
            if (! $disk->exists($safe) && ! $disk->directoryExists($safe)) {
                throw new RuntimeException("Path bulunamadı: {$p}");
            }
            $safePaths[] = $disk->path($safe);
        }

        // Production: delegate to sysapi
        if (! app()->environment('local', 'testing')) {
            $result = Sysapi::run('file-chmod', [
                'paths' => $safePaths,
                'mode' => $mode,
                'recursive' => $recursive,
            ]);
            if (! $result->ok()) {
                throw new RuntimeException('chmod başarısız: '.$result->stderr);
            }

            return;
        }

        // Dev/test: apply directly via PHP
        $octal = octdec(ltrim($mode, '0') ?: '0');
        foreach ($safePaths as $abs) {
            if ($recursive && is_dir($abs)) {
                $iterator = new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator($abs, \RecursiveDirectoryIterator::SKIP_DOTS),
                    \RecursiveIteratorIterator::SELF_FIRST,
                );
                foreach ($iterator as $item) {
                    @chmod($item->getPathname(), $octal);
                }
            }
            @chmod($abs, $octal);
        }
    }

    /**
     * Dosya/klasör sahibini değiştir.
     * Sadece hesabın kendi UID/GID'i ile sınırlıdır.
     *
     * @param  array<string>  $paths  Relative paths under account home
     */
    public function chown(Account $account, array $paths, int $uid, int $gid, bool $recursive = false): void
    {
        // Guard: uid/gid must match the account's own system user
        $accountUid = $account->uid ?? null;
        $accountGid = $account->gid ?? $accountUid;

        if ($accountUid !== null && ($uid !== $accountUid || $gid !== $accountGid)) {
            throw new RuntimeException('Yalnızca kendi UID/GID değerlerinizi kullanabilirsiniz.');
        }

        $disk = $this->disk($account);
        $safePaths = [];
        foreach ($paths as $p) {
            $safe = $this->safe($p);
            if (! $disk->exists($safe) && ! $disk->directoryExists($safe)) {
                throw new RuntimeException("Path bulunamadı: {$p}");
            }
            $safePaths[] = $disk->path($safe);
        }

        // Production: delegate to sysapi
        if (! app()->environment('local', 'testing')) {
            $result = Sysapi::run('file-chown', [
                'paths' => $safePaths,
                'uid' => $uid,
                'gid' => $gid,
                'recursive' => $recursive,
            ]);
            if (! $result->ok()) {
                throw new RuntimeException('chown başarısız: '.$result->stderr);
            }
        }
        // Dev/test: no-op (chown requires root — skip silently)
    }

    /**
     * Return octal permission string for a filesystem path, e.g. "0644".
     * Returns empty string if stat unavailable (Windows dev env).
     */
    private function filePermissions(string $absPath): string
    {
        if (! file_exists($absPath)) {
            return '';
        }
        $perms = @fileperms($absPath);
        if ($perms === false) {
            return '';
        }

        return sprintf('%04o', $perms & 0777);
    }
}
