<?php 
/**
 * Luminova Framework
 *
 * @package Luminova
 * @author Ujah Chigozie Peter
 * @copyright (c) Nanoblock Technology Ltd
 * @license See LICENSE file
 * @link https://luminova.ng
 */
namespace Luminova\Ftp;

use \FTP\Connection;
use \RecursiveIteratorIterator;
use \RecursiveDirectoryIterator;
use \Luminova\Exceptions\RuntimeException;
use \Luminova\Exceptions\InvalidArgumentException;
use function \Luminova\Funcs\{
    write_content,
    get_temp_file
};

class FTP
{
    private Connection|false $connection = false;
    private int $mode = FTP_BINARY;
    private bool $isConnected = false;
    private string $remoteDir = '/';
    private array $failed = [];
    private string $error = '';

    public function __construct(private array $config) {}

    public function __call(string $method, array $arguments): mixed
    {
        $this->assertConnection();

        $function = 'ftp_' . $this->toSnakeCase($method);
        if (function_exists($function)) {
            $result = $function($this->connection, ...$arguments);

            if(!$result){
                $this->error = 'Failed to complete ftp "' . ltrim($function, 'ftp_'). '" request.';
            }

            return $result;
        }

        throw new RuntimeException(sprintf(
            'The FTP method: "%s" is not available or function: "%s" is invalid.',
            $method,
            $function
        ));
    }

    private function toSnakeCase(string $string): string
    {
        $result = str_contains($string, '_') 
            ? $string 
            : preg_replace('/([a-z])([A-Z])/', '$1_$2', $string);

        return strtolower($result);
    }

    public function connect(): bool
    {
        if ($this->isConnected()) {
            return true;
        }

        $ssl = $this->config['ssl'] ?? false;
        $client = $ssl ? 'ftp_ssl_connect' : 'ftp_connect';

        $this->connection = $client(
            $this->config['host'],
            $this->config['port'] ?? 21,
            $this->config['timeout'] ?? 90
        );
       
        if (!$this->connection) {
            throw new RuntimeException("Could not connect to {$this->config['host']}");
        }

        if (isset($this->config['username']) && !ftp_login(
            $this->connection, 
            $this->config['username'],
            $this->config['password']
        )) {
            ftp_close($this->connection);
            throw new RuntimeException("Failed to log in to {$this->config['host']} with the provided credentials.");
        }

        $this->passive();
        $this->isConnected = true;

        return true;
    }

    public function passive(bool $enable = true): bool 
    {
        $this->assertConnection();
        return ftp_pasv($this->connection, $enable);
    }

    public function option(int $option, mixed $value): bool 
    {
        $this->assertConnection();
        return ftp_set_option($this->connection, $option, $value);
    }

    public function options(array $options): bool 
    {
        foreach($options as $option => $value) {
            if(!$this->option($option, $value)){
                return false;
            }
        }

        return true;
    }

    public function disconnect(): bool
    {
        if (!$this->isConnected()) {
            return false;
        }

        $result = ftp_close($this->connection);
        $this->isConnected = !$result;
        $this->connection = !$result;
        $this->remoteDir = '/';
        return $result;
    }

    public function isConnected(): bool
    {
        return $this->isConnected && $this->connection !== false;
    }

    public function getError(): array 
    {
        return ['message' => $this->error, 'files' => $this->failed];
    }

    private function getDirectory(?string $arguments = null, ?string $filter = null): string 
    {
        if(!$this->isDir($this->remoteDir)){
            throw new RuntimeException('"'.$this->remoteDir.'" is not a directory');
        }

        $path = ($arguments ? $arguments . ' ' : '') . $this->remoteDir;
        $path .= $filter ? $filter : '';

        return $path;
    }

    private function getTempfile(string $prefix): string|bool 
    {
        return tempnam(sys_get_temp_dir(), $prefix)?: get_temp_file($prefix, 'temp', true);
    }


    public function mode(int $mode = FTP_BINARY): self
    {
        $this->mode = $mode;
        return $this;
    } 

    public function dir(string $remoteDir): self
    {
        $this->remoteDir = rtrim($remoteDir, TRIM_DS) . DIRECTORY_SEPARATOR;
        return $this;
    }

    function chdir(string $dir): bool
    {
        $this->assertConnection();
        $this->dir($dir);

        return ftp_chdir($this->connection, $this->remoteDir);
    }

    function pwd(): string
    {
        $this->assertConnection();

        return ftp_pwd($this->connection) ?: '';
    }

    public function modified(string $file, ?string $format = null)
    {
        $this->assertConnection();
        $timestamp = ftp_mdtm($this->connection, $this->remoteDir . $file);

        if ($format && $timestamp !== -1) {
            return date($format, $timestamp);
        }

        return $timestamp;
    }

    public function isDir(string $directory): bool
    {
        $pwd = $this->pwd();

        if (!$pwd) {
            throw new RuntimeException('Unable to resolve the current directory');
        }

        if ($this->chdir($directory)) {
            $this->chdir($pwd);
            return true;
        }

        $this->chdir($pwd);

        return false;
    }

    public function upload(string $localPath): bool
    {
        $this->assertConnection();
        $this->failed = [];

        if (!is_dir($localPath)) {
            throw new InvalidArgumentException("The provided path is not a directory: $localPath");
        }

        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($localPath, RecursiveDirectoryIterator::SKIP_DOTS)
        );

        $uploaded = 0;
        foreach ($iterator as $file) {
            $remoteFilePath = $this->remoteDir . str_replace($localPath . DIRECTORY_SEPARATOR, '', $file);
            if (!ftp_put($this->connection, $remoteFilePath, $file, $this->mode)) {
                $this->failed[] = $file;
            }else{
                $uploaded++;
            }
        }

        if($this->failed !== []){
            $this->error = 'Failed to upload '. count($this->failed) . ' files';
        }

        return $uploaded > 0;
    }

    public function put(string $file): bool
    {
        $this->assertConnection();
        if (!file_exists($file)) {
            throw new InvalidArgumentException("Local file does not exist: $file");
        }

        return ftp_put($this->connection, $this->remoteDir . basename($file), $file, $this->mode);
    }

    public function touch(string $file): bool
    {
        return $this->write($file, '');
    }

    public function write(string $file, string $content): bool
    {
        $this->assertConnection();
        $tempFile = $this->getTempfile('ftp_write_');

        if(!$tempFile || ($tempFile && $content && !write_content($tempFile, $content))){
            return false;
        }

        $status = ftp_put($this->connection, $this->remoteDir . basename($file), $tempFile, $this->mode);
        unlink($tempFile);
        return $status;
    }

    public function list(?string $arguments = null, ?string $filter = null): array
    {
        $this->assertConnection();
        $files = ftp_nlist($this->connection, $this->getDirectory($arguments, $filter));
        
        return ($files === false) ? [] : $files;
    }

    public function scan(
        ?string $arguments = null, 
        ?string $filter = null, 
        bool $recursive = false
    ): array
    {
        $this->assertConnection();
        $list = ftp_rawlist($this->connection, $this->getDirectory($arguments, $filter), $recursive);
        return ($list === false) ? [] : $list;
    }

    public function files(?string $arguments = null, ?string $filter = null): array 
    {
        $files = $this->list($arguments, $filter);

        return ($files === []) ? [] : array_values(array_filter($files, function($file) {
            return !preg_match('/\/$/', $file);
        }));
    }

    public function delete(string $file): bool
    {
        $this->assertConnection();
        return ftp_delete($this->connection, $this->remoteDir . $file);
    }

    public function clear(): bool
    {
        $files = $this->list();
        $this->failed = [];

        foreach ($files as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            if (!ftp_delete($this->connection, $file)) {
                $this->failed[] = $file;
            }
        }

        if($this->failed !== []){
            $this->error = 'Failed to delete '. count($this->failed) . ' files';
        }

        return $files !== [];
    }

    public function rename(string $old, string $new): bool
    {
        return $this->exists($old) 
            ? ftp_rename($this->connection, $this->remoteDir . $old, $this->remoteDir . $new)
            : false;
    }

    public function move(string $file, string $to): bool
    {
        return $this->rename($file, $to);
    }

    public function copy(string $file, string $to): bool
    {
        if(!$this->exists($file)){
            return false;
        }

        $tempFile = $this->getTempfile('ftp_copy_');

        if(!$tempFile){
            $this->error = 'Failed to create temporary file.';
            return false;
        }

        if (!ftp_get($this->connection, $tempFile, $this->remoteDir . $file, $this->mode)) {
            return false;
        }

        $result = ftp_put($this->connection, $this->remoteDir . $to, $tempFile, $this->mode);
        unlink($tempFile);

        return $result;
    }

    public function mkdir(string $path): bool
    {
        $this->assertConnection();
        return ftp_mkdir($this->connection, $this->remoteDir . $path) !== false;
    }

    public function rmdir(): bool
    {
        $files = $this->list();

        if ($files === []) {
            $this->error = "Unable to list files in directory: {$this->remoteDir}";
            return false;
        }

        $dir = $this->remoteDir;
        
        foreach ($files as $file) {
            $deleted = false;

            if (is_dir($file)) {
                $child = $this->dir($file);
                $child->list();
                $deleted = $child->rmdir();

                $this->remoteDir = $dir;
            } else {
                $deleted = ftp_delete($this->connection, $file);
            }

            if(!$deleted){
                $this->failed[] = $file;
            }
        }

        if (!ftp_rmdir($this->connection, $this->remoteDir)) {
            $this->failed[] = $this->remoteDir;
            $this->error = "Failed to remove directory: {$this->remoteDir}";

            return false;
        } 

        return true;
    }

    public function count(?string $file = null): int
    {
        $this->assertConnection();
        return ftp_size($this->connection, $this->remoteDir . ($file ?? ''));
    }

    public function exists(string $file): bool
    {
        return $this->count($file) !== -1;
    }

    private function assertConnection(): void 
    {
        if (!$this->connection) {
            throw new RuntimeException("FTP is not connected to {$this->config['host']}");
        }
    }
}
