<?php

declare(strict_types=1);

namespace PpPhp\Core\Deploy;

/**
 * SFTP transport built on libcurl's SFTP support (no ext-ssh2 required).
 *
 * Config keys (see TransportFactory):
 *   host, port, user, pass, privateKeyFile, privateKeyPass, timeout
 */
class SftpTransport implements TransportInterface
{
    // libcurl error codes. Several are NOT exposed as PHP constants in every
    // build (e.g. CURLE_REMOTE_FILE_NOT_FOUND / CURLE_LOGIN_DENIED), so we use
    // the stable numeric values directly to avoid "undefined constant" errors.
    private const E_COULDNT_CONNECT       = 7;
    private const E_REMOTE_ACCESS_DENIED  = 9;
    private const E_FTP_COULDNT_RETR_FILE = 19;
    private const E_OPERATION_TIMEDOUT    = 28;
    private const E_LOGIN_DENIED          = 67;
    private const E_REMOTE_FILE_NOT_FOUND = 78;
    private const E_SSH                   = 79;

    private string $base;   // sftp://host:port

    public function __construct(private array $config)
    {
        $host = $this->resolveHost((string) ($this->config['host'] ?? ''));
        $port = (int) ($this->config['port'] ?? 22) ?: 22;
        $this->base = 'sftp://' . $host . ':' . $port;
    }

    /** Let the user type a bare domain; drop a stray "ftp." prefix if needed. */
    private function resolveHost(string $host): string
    {
        $host = trim($host);
        if ($host === '' || filter_var($host, FILTER_VALIDATE_IP)) {
            return $host;
        }
        $candidates = [$host];
        if (str_starts_with($host, 'ftp.')) {
            $candidates[] = substr($host, 4);
        }
        foreach ($candidates as $c) {
            $ip = @gethostbyname($c);
            if ($ip !== $c && filter_var($ip, FILTER_VALIDATE_IP)) {
                return $c;
            }
        }
        return $host;
    }

    public function protocol(): string
    {
        return 'sftp';
    }

    public function connect(): void
    {
        $this->assertCapable();
        if (($this->config['host'] ?? '') === '') {
            throw new TransportException('No SFTP host was provided.');
        }

        $ch = $this->handle($this->base . '/');
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_exec($ch);
        $errno = curl_errno($ch);
        $err   = curl_error($ch);
        curl_close($ch);

        if ($errno !== 0 && !in_array($errno, [self::E_REMOTE_FILE_NOT_FOUND, self::E_FTP_COULDNT_RETR_FILE], true)) {
            throw new TransportException($this->explain($errno, $err));
        }
    }

    public function diagnose(): array
    {
        $host    = (string) ($this->config['host'] ?? '');
        $port    = (int) ($this->config['port'] ?? 22) ?: 22;
        $timeout = (int) ($this->config['timeout'] ?? 20) ?: 20;
        $stages  = [];

        $capable = extension_loaded('curl') && in_array('sftp', curl_version()['protocols'] ?? [], true);
        $stages[] = ['stage' => 'cURL SFTP support', 'ok' => $capable, 'detail' => $capable ? 'Available' : 'PHP cURL was built without SFTP support'];
        if (!$capable) {
            return $this->report($stages);
        }

        // DNS
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            $stages[] = ['stage' => 'DNS', 'ok' => true, 'detail' => "Literal IP {$host}"];
        } else {
            $ip = @gethostbyname($host);
            $ok = $ip !== $host && filter_var($ip, FILTER_VALIDATE_IP);
            $stages[] = ['stage' => 'DNS', 'ok' => (bool) $ok, 'detail' => $ok ? "{$host} → {$ip}" : "Could not resolve host '{$host}'."];
        }

        // TCP
        $errno = 0; $errstr = '';
        $sock = @fsockopen($host, $port, $errno, $errstr, min($timeout, 10));
        if ($sock) {
            fclose($sock);
            $stages[] = ['stage' => 'TCP reachability', 'ok' => true, 'detail' => "Port {$port} is open"];
        } else {
            $stages[] = ['stage' => 'TCP reachability', 'ok' => false, 'detail' => "Port {$port} unreachable — {$errstr} (errno {$errno})."];
            return $this->report($stages);
        }

        // Auth handshake
        $ch = $this->handle($this->base . '/');
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_exec($ch);
        $cerr = curl_errno($ch);
        $cmsg = curl_error($ch);
        curl_close($ch);
        $authOk = $cerr === 0 || in_array($cerr, [self::E_REMOTE_FILE_NOT_FOUND, self::E_FTP_COULDNT_RETR_FILE], true);
        $usingKey = ($this->config['privateKeyFile'] ?? '') !== '';
        $stages[] = [
            'stage'  => 'SSH authentication',
            'ok'     => $authOk,
            'detail' => $authOk
                ? ('Authenticated as ' . ($this->config['user'] ?? '') . ($usingKey ? ' (key)' : ' (password)'))
                : $this->explain($cerr, $cmsg),
        ];

        return $this->report($stages);
    }

    public function ensureDir(string $remoteDir): void
    {
        // Directories are created lazily by uploads (CURLFTP_CREATE_DIR).
    }

    public function put(string $localPath, string $remotePath): bool
    {
        $fp = @fopen($localPath, 'rb');
        if ($fp === false) {
            return false;
        }
        $ch = $this->handle($this->base . '/' . ltrim($remotePath, '/'), 300);
        curl_setopt($ch, CURLOPT_UPLOAD, true);
        curl_setopt($ch, CURLOPT_INFILE, $fp);
        curl_setopt($ch, CURLOPT_INFILESIZE, (int) filesize($localPath));
        curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR);
        curl_exec($ch);
        $ok = curl_errno($ch) === 0;
        curl_close($ch);
        fclose($fp);
        return $ok;
    }

    public function listDir(string $remoteDir): array
    {
        $dir = '/' . trim($remoteDir, '/');
        if ($dir !== '/') {
            $dir .= '/';
        }
        $ch = $this->handle($this->base . $dir);
        // No DIRLISTONLY → curl returns an `ls -l` style listing we can parse.
        $body = curl_exec($ch);
        $errno = curl_errno($ch);
        curl_close($ch);
        if ($errno !== 0 || !is_string($body)) {
            return [];
        }

        $out = [];
        foreach (preg_split('/\r?\n/', $body) as $line) {
            $line = trim($line);
            if ($line === '' || str_starts_with($line, 'total ')) {
                continue;
            }
            $entry = $this->parseUnixListLine($line);
            if ($entry !== null) {
                $out[] = $entry;
            }
        }
        return $this->sortEntries($out);
    }

    public function makeDir(string $remoteDir): bool
    {
        return $this->quote('mkdir "' . $this->esc($remoteDir) . '"');
    }

    public function rename(string $from, string $to): bool
    {
        return $this->quote('rename "' . $this->esc($from) . '" "' . $this->esc($to) . '"');
    }

    public function removeDir(string $remoteDir): bool
    {
        return $this->quote('rmdir "' . $this->esc($remoteDir) . '"');
    }

    public function stat(string $remotePath): ?array
    {
        $ch = $this->handle($this->base . '/' . ltrim($remotePath, '/'));
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch, CURLOPT_FILETIME, true);
        curl_exec($ch);
        if (curl_errno($ch) !== 0) {
            curl_close($ch);
            return null;
        }
        $size  = (int) curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
        $mtime = (int) curl_getinfo($ch, CURLINFO_FILETIME);
        curl_close($ch);
        return ['size' => $size >= 0 ? $size : 0, 'mtime' => $mtime > 0 ? $mtime : null];
    }

    public function delete(string $remotePath): bool
    {
        return $this->quote('rm "' . $this->esc($remotePath) . '"');
    }

    public function close(): void
    {
        // Per-request handles are closed inline; nothing persistent to release.
    }

    // ── helpers ─────────────────────────────────────────────────────

    /** Run one or more SFTP quote commands from the connection root. */
    private function quote(string $command): bool
    {
        $ch = $this->handle($this->base . '/');
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch, CURLOPT_QUOTE, [$command]);
        curl_exec($ch);
        $ok = curl_errno($ch) === 0;
        curl_close($ch);
        return $ok;
    }

    private function esc(string $path): string
    {
        $path = '/' . ltrim($path, '/');
        return str_replace('"', '\\"', $path);
    }

    private function assertCapable(): void
    {
        if (!extension_loaded('curl')) {
            throw new TransportException('SFTP requires the PHP cURL extension, which is not installed.');
        }
        if (!in_array('sftp', curl_version()['protocols'] ?? [], true)) {
            throw new TransportException('This build of PHP/cURL was compiled without SFTP support.');
        }
    }

    private function handle(string $url, int $timeout = 30)
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_SFTP);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) ($this->config['timeout'] ?? 20) ?: 20);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

        $user = (string) ($this->config['user'] ?? '');
        $keyFile = (string) ($this->config['privateKeyFile'] ?? '');
        if ($keyFile !== '' && is_file($keyFile)) {
            curl_setopt($ch, CURLOPT_SSH_AUTH_TYPES, CURLSSH_AUTH_PUBLICKEY);
            curl_setopt($ch, CURLOPT_SSH_PRIVATE_KEYFILE, $keyFile);
            if (($this->config['privateKeyPass'] ?? '') !== '') {
                curl_setopt($ch, CURLOPT_KEYPASSWD, (string) $this->config['privateKeyPass']);
            }
            curl_setopt($ch, CURLOPT_USERNAME, $user);
        } else {
            curl_setopt($ch, CURLOPT_SSH_AUTH_TYPES, CURLSSH_AUTH_PASSWORD);
            curl_setopt($ch, CURLOPT_USERPWD, $user . ':' . (string) ($this->config['pass'] ?? ''));
        }
        return $ch;
    }

    private function parseUnixListLine(string $line): ?array
    {
        if (!preg_match('/^([\-dl])[rwxsStT\-]{9}[\.\+]?\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w{3}\s+\d+\s+[\d:]+)\s+(.+)$/', $line, $m)) {
            return null;
        }
        $name = $m[4];
        if ($name === '.' || $name === '..') {
            return null;
        }
        $type = $m[1] === 'd' ? 'dir' : ($m[1] === 'l' ? 'link' : 'file');
        if ($type === 'link') {
            $name = preg_replace('/\s+->\s+.*$/', '', $name);
        }
        $ts = strtotime($m[3]) ?: null;
        return ['name' => $name, 'type' => $type, 'size' => (int) $m[2], 'mtime' => $ts];
    }

    private function sortEntries(array $entries): array
    {
        usort($entries, static function ($a, $b) {
            if ($a['type'] !== $b['type']) {
                return $a['type'] === 'dir' ? -1 : 1;
            }
            return strcasecmp($a['name'], $b['name']);
        });
        return $entries;
    }

    private function report(array $stages): array
    {
        $ok = array_reduce($stages, static fn($c, $s) => $c && $s['ok'], true);
        $failed = array_values(array_filter($stages, static fn($s) => !$s['ok']));
        $summary = $ok
            ? 'All checks passed — the server is reachable and the credentials work.'
            : ($failed[0]['stage'] . ' failed: ' . $failed[0]['detail']);
        return ['ok' => $ok, 'summary' => $summary, 'stages' => $stages];
    }

    private function explain(int $errno, string $err): string
    {
        return match ($errno) {
            self::E_COULDNT_CONNECT, self::E_OPERATION_TIMEDOUT =>
                'Could not connect to the SFTP server — check the host, port and firewall.',
            self::E_LOGIN_DENIED, self::E_REMOTE_ACCESS_DENIED =>
                'SFTP login was denied. Use your SSH account — on cPanel/shared hosting that is the MAIN account username (not an FTP sub-account like "user@domain.com"), and SSH access may need to be enabled by your host first.',
            self::E_SSH =>
                'SSH/SFTP handshake failed: ' . ($err !== '' ? $err : 'the server rejected the key or credentials'),
            default =>
                'SFTP error: ' . ($err !== '' ? $err : ('code ' . $errno)),
        };
    }
}
