<?php

declare(strict_types=1);

namespace Lab;

final class Output
{
    private string $lastPrintError = '';

    public function __construct(private array $config, private OrderStore $orders, private Products $products) {}

    public function saveFiles(array $order): string
    {
        $settings = Settings::get();
        $exportDir = $this->orders->exportHotfolder($order, $this->products->map());
        $destRoot = trim((string) $settings['save_dir']);
        if ($destRoot === '') {
            $destRoot = rtrim((string) ($this->config['print_dir'] ?? ($this->config['orders_dir'] . '/saved')), '/');
        }
        $dest = rtrim($destRoot, '/\\') . DIRECTORY_SEPARATOR . Helpers::orderFolderName($order);
        $this->copyTree($exportDir, $dest);

        return $dest;
    }

    /**
     * Copies to DNP hotfolder (if set) and prints via the Windows spooler.
     *
     * @return array{printed:int, copied:string, folder:string, printer:string, size:string}
     */
    public function sendDnp(array $order, ?string $printer = null, ?string $sku = null): array
    {
        $settings = Settings::get();
        $products = $this->products->map();
        $exportDir = $this->orders->exportHotfolder($order, $products);
        $copied = '';
        $hot = trim((string) $settings['dnp_hotfolder']);
        if ($hot !== '') {
            $copied = rtrim($hot, '/\\') . DIRECTORY_SEPARATOR . $order['code'];
            $this->copyTree($exportDir, $copied);
            $this->copyDnpSizeAliases($exportDir, rtrim($hot, '/\\'), (string) $order['code'], $products);
        }
        $printed = 0;
        $override = $printer !== null ? trim($printer) : '';
        $filter = $sku !== null && $sku !== '' && $sku !== 'all' ? $sku : null;
        $job = $this->jobFiles($exportDir, $order);
        $used = [];
        foreach ($job as $item) {
            if ($filter !== null && $item['sku'] !== $filter) {
                continue;
            }
            $product = $products[$item['sku']] ?? [];
            $product['sku'] = $item['sku'];
            $chosen = $this->printerForSku((string) $item['sku'], $override, $settings);
            $used[] = $chosen === '' ? (PHP_OS_FAMILY === 'Darwin' ? 'macOS' : 'Windows') : $chosen;
            for ($i = 0; $i < $item['qty']; $i++) {
                if ($this->printFile($item['path'], $chosen, $product)) {
                    $printed++;
                }
            }
        }
        if ($printed === 0) {
            $hint = trim($this->lastPrintError);
            throw new \RuntimeException($hint !== ''
                ? 'Δεν στάλθηκε στον εκτυπωτή: ' . $hint
                : 'Δεν στάλθηκε στον εκτυπωτή. Έλεγξε ότι είναι ανοιχτός και ότι η διάσταση έχει σωστό εκτυπωτή στις ρυθμίσεις.');
        }

        return [
            'printed' => $printed,
            'copied' => $copied,
            'folder' => $exportDir,
            'printer' => implode(', ', array_unique($used)),
            'size' => $filter ?? 'all',
        ];
    }

    /** @return list<array{id:string,name:string,inch:string,qty:int}> */
    public function sizesInOrder(array $order): array
    {
        $products = $this->products->map();
        $items = json_decode((string) ($order['items_json'] ?? '[]'), true);
        if (! is_array($items)) {
            $items = [];
        }
        $qty = [];
        foreach ($items as $item) {
            foreach ($item['lines'] ?? [] as $line) {
                $sku = (string) ($line['sku'] ?? '');
                if ($sku === '') {
                    continue;
                }
                $qty[$sku] = ($qty[$sku] ?? 0) + max(1, (int) ($line['qty'] ?? 1));
            }
        }
        $out = [[
            'id' => 'all',
            'name' => 'Όλα τα μεγέθη της παραγγελίας',
            'inch' => '',
            'qty' => array_sum($qty),
        ]];
        foreach ($qty as $sku => $n) {
            $product = $products[$sku] ?? null;
            $inch = $product ? $this->inchLabel($product + ['sku' => $sku]) : '';
            $name = is_array($product) ? (string) $product['name'] : $sku;
            $out[] = [
                'id' => $sku,
                'name' => $inch !== '' ? $inch . ' · ' . $name : $name,
                'inch' => $inch,
                'qty' => $n,
            ];
        }

        return $out;
    }

    /** @return list<array{id:string,name:string,default:bool,kind:string,port:string,search:string}> */
    public static function listPrinters(): array
    {
        $settings = Settings::get();
        $saved = trim((string) ($settings['dnp_printer'] ?? ''));
        $network = trim((string) ($settings['dnp_network'] ?? ''));
        $rows = PHP_OS_FAMILY === 'Windows' ? self::windowsPrinterRows() : self::unixPrinterRows();
        $out = [];
        $seen = [];
        foreach ($rows as $row) {
            $name = trim((string) ($row['name'] ?? ''));
            if ($name === '' || isset($seen[$name])) {
                continue;
            }
            $seen[$name] = true;
            $kind = (string) ($row['kind'] ?? 'installed');
            $port = (string) ($row['port'] ?? '');
            $out[] = [
                'id' => $name,
                'name' => $name,
                'default' => $saved !== '' && $name === $saved,
                'kind' => $kind,
                'port' => $port,
                'search' => strtolower($name . ' ' . $port . ' ' . $kind),
            ];
        }
        foreach ([$saved, $network] as $extra) {
            if ($extra === '' || isset($seen[$extra])) {
                continue;
            }
            $seen[$extra] = true;
            array_unshift($out, [
                'id' => $extra,
                'name' => $extra,
                'default' => $saved !== '' && $extra === $saved,
                'kind' => 'network',
                'port' => $extra,
                'search' => strtolower($extra . ' network'),
            ]);
        }
        array_unshift($out, [
            'id' => '',
            'name' => PHP_OS_FAMILY === 'Darwin' ? 'Προεπιλογή macOS' : 'Προεπιλογή Windows',
            'default' => $saved === '',
            'kind' => 'default',
            'port' => '',
            'search' => 'προεπιλογή default macos windows',
        ]);

        return $out;
    }

    /** @param array<string, mixed> $settings */
    private function printerForSku(string $sku, string $override, array $settings): string
    {
        if ($override !== '') {
            return $override;
        }
        $map = $settings['printer_by_sku'] ?? [];
        if (is_array($map)) {
            $named = trim((string) ($map[$sku] ?? ''));
            if ($named !== '') {
                return $named;
            }
        }

        return trim((string) ($settings['dnp_printer'] ?? ''));
    }

    /** @return list<array{name:string,kind:string,port:string}> */
    private static function windowsPrinterRows(): array
    {
        $cmd = 'powershell -NoProfile -NonInteractive -Command "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); Get-CimInstance Win32_Printer | ForEach-Object { $_.Name + [char]9 + [int]$_.Network + [char]9 + $_.PortName + [char]9 + [int]$_.Shared }"';
        exec($cmd, $lines, $code);
        $rows = [];
        foreach ($lines as $line) {
            $line = trim((string) $line);
            if ($line === '' || preg_match('/^Name$/i', $line)) {
                continue;
            }
            $parts = explode("\t", $line);
            $name = trim((string) ($parts[0] ?? ''));
            if ($name === '') {
                continue;
            }
            $port = trim((string) ($parts[2] ?? ''));
            $network = ((int) ($parts[1] ?? 0)) === 1
                || str_contains($name, '\\')
                || str_starts_with($port, 'IP_')
                || str_starts_with($port, 'WSD')
                || str_contains($port, '\\');
            $rows[] = [
                'name' => $name,
                'kind' => $network ? 'network' : 'installed',
                'port' => $port,
            ];
        }
        if ($rows === []) {
            $fallback = [];
            exec('powershell -NoProfile -NonInteractive -Command "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); Get-CimInstance Win32_Printer | ForEach-Object { $_.Name }"', $fallback);
            foreach ($fallback as $line) {
                $name = trim((string) $line);
                if ($name !== '' && ! preg_match('/^Name$/i', $name)) {
                    $rows[] = ['name' => $name, 'kind' => 'installed', 'port' => ''];
                }
            }
        }

        return $rows;
    }

    private static function unixPathPrefix(): string
    {
        return 'LANG=C LC_ALL=C PATH=/usr/bin:/bin:/usr/sbin:/sbin ';
    }

    private static function lpstatBin(): string
    {
        foreach (['/usr/bin/lpstat', '/usr/sbin/lpstat'] as $bin) {
            if (is_file($bin)) {
                return $bin;
            }
        }

        return 'lpstat';
    }

    /** @return list<string> */
    private static function lpstatLines(string $args): array
    {
        $lines = [];
        exec(self::unixPathPrefix() . escapeshellarg(self::lpstatBin()) . ' ' . $args . ' 2>/dev/null', $lines);

        return $lines;
    }

    /** @return list<array{name:string,kind:string,port:string}> */
    private static function unixPrinterRows(): array
    {
        $names = [];
        $ports = [];
        foreach (self::lpstatLines('-p') as $line) {
            $line = (string) $line;
            if (preg_match('/^printer\s+(.+?)\s+(is|disabled|enabled|was)\b/i', $line, $m)
                || preg_match('/^ο εκτυπωτής\s+(.+?)\s+είναι\b/iu', $line, $m)) {
                $names[] = trim($m[1]);
            }
        }
        foreach (self::lpstatLines('-e') as $line) {
            $line = trim((string) $line);
            if ($line !== '' && ! preg_match('/^(το|ο|η)$/iu', $line)) {
                $names[] = $line;
            }
        }
        foreach (self::lpstatLines('-a') as $line) {
            $line = (string) $line;
            if (preg_match('/^(\S+)\s+accepting\b/i', $line, $m)
                || preg_match('/^το\s+(\S+)\s+αποδέχεται\b/iu', $line, $m)) {
                $names[] = $m[1];
            }
        }
        foreach (self::lpstatLines('-v') as $line) {
            $line = (string) $line;
            if (preg_match('/^device for\s+(.+?):\s*(.*)$/i', $line, $m)
                || preg_match('/^συσκευή για\s+(.+?):\s*(.*)$/iu', $line, $m)) {
                $name = trim($m[1]);
                $names[] = $name;
                $ports[$name] = trim($m[2]);
            }
        }

        $out = [];
        $seen = [];
        foreach ($names as $name) {
            $name = trim((string) $name);
            if ($name === '' || isset($seen[$name]) || preg_match('/^(printer|device|scheduler|no|the|το|ο|η)$/iu', $name)) {
                continue;
            }
            $seen[$name] = true;
            $port = $ports[$name] ?? '';
            $uri = strtolower($port);
            $network = str_contains($uri, 'ipp://')
                || str_contains($uri, 'ipps://')
                || str_contains($uri, 'socket://')
                || str_contains($uri, 'lpd://')
                || str_contains($uri, 'dnssd://')
                || str_contains($name, '.');
            $out[] = [
                'name' => $name,
                'kind' => $network ? 'network' : 'installed',
                'port' => $port,
            ];
        }

        return $out;
    }

    /** @return list<array{path:string,qty:int,sku:string}> */
    private function jobFiles(string $exportDir, array $order): array
    {
        $items = json_decode((string) ($order['items_json'] ?? '[]'), true);
        if (! is_array($items)) {
            $items = [];
        }
        $out = [];
        foreach ($items as $item) {
            foreach ($item['lines'] ?? [] as $line) {
                $sku = (string) ($line['sku'] ?? '');
                $qty = max(1, (int) ($line['qty'] ?? 1));
                $base = pathinfo((string) $item['original_name'], PATHINFO_FILENAME);
                $base = preg_replace('/[^\w.-]+/', '_', $base) ?: 'photo';
                $path = $exportDir . DIRECTORY_SEPARATOR . $sku . DIRECTORY_SEPARATOR . $base . '_x' . $qty . '.jpg';
                if (is_file($path)) {
                    $out[] = ['path' => $path, 'qty' => $qty, 'sku' => $sku];
                }
            }
        }

        return $out;
    }

    /** @param array<string, mixed> $product */
    private function printFile(string $path, string $printer, array $product): bool
    {
        if (PHP_OS_FAMILY === 'Darwin' || PHP_OS_FAMILY === 'Linux') {
            return $this->printFileUnix($path, $printer, $product);
        }
        if (PHP_OS_FAMILY !== 'Windows') {
            return false;
        }
        $file = str_replace('/', '\\', $path);
        $script = $this->printScript();
        if ($script !== '' && is_file($script)) {
            $wmm = (int) round(((float) ($product['width_cm'] ?? 10)) * 10);
            $hmm = (int) round(((float) ($product['height_cm'] ?? 15)) * 10);
            $info = @getimagesize($path);
            if (is_array($info) && (int) $info[0] > 0 && (int) $info[1] > 0) {
                [$wcm, $hcm] = Photos::paperCm($product, (int) $info[0], (int) $info[1]);
                $wmm = (int) round($wcm * 10);
                $hmm = (int) round($hcm * 10);
            }
            $job = [
                'file' => $file,
                'printer' => $printer,
                'width_mm' => $wmm,
                'height_mm' => $hmm,
                'copies' => 1,
                'label' => $this->inchLabel($product),
            ];
            $tmp = tempnam(sys_get_temp_dir(), 'labpr') ?: (sys_get_temp_dir() . '/labpr.json');
            $json = $tmp . '.json';
            file_put_contents($json, json_encode($job, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
            $cmd = 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File '
                . escapeshellarg($script) . ' -Job ' . escapeshellarg($json);
            exec($cmd, $ignore, $code);
            @unlink($json);
            @unlink($tmp);
            if ($code === 0) {
                return true;
            }
        }
        if ($printer === '') {
            $cmd = 'mspaint /p ' . escapeshellarg($file);
        } else {
            $cmd = 'mspaint /pt ' . escapeshellarg($file) . ' ' . escapeshellarg($printer);
        }
        exec($cmd, $ignore, $code);

        return $code === 0;
    }

    /** @param array<string, mixed> $product */
    private function printFileUnix(string $path, string $printer, array $product): bool
    {
        $this->lastPrintError = '';
        $lp = is_file('/usr/bin/lp') ? '/usr/bin/lp' : 'lp';
        $lpr = is_file('/usr/bin/lpr') ? '/usr/bin/lpr' : 'lpr';
        $dest = $printer !== '' ? ' -d ' . escapeshellarg($printer) : '';
        $pdest = $printer !== '' ? ' -P ' . escapeshellarg($printer) : '';
        $file = escapeshellarg($path);
        $prefix = self::unixPathPrefix();
        $inch = $this->inchLabel($product);
        $wmm = (int) round(((float) ($product['width_cm'] ?? 10)) * 10);
        $hmm = (int) round(((float) ($product['height_cm'] ?? 15)) * 10);
        $info = @getimagesize($path);
        if (is_array($info) && (int) $info[0] > 0 && (int) $info[1] > 0) {
            [$wcm, $hcm] = Photos::paperCm($product, (int) $info[0], (int) $info[1]);
            $wmm = (int) round($wcm * 10);
            $hmm = (int) round($hcm * 10);
        }
        $optSets = [];
        if ($inch !== '') {
            $optSets[] = ' -o ' . escapeshellarg('media=' . $inch);
            $optSets[] = ' -o ' . escapeshellarg('PageSize=' . $inch);
            $optSets[] = ' -o ' . escapeshellarg('media=' . $inch) . ' -o fit-to-page';
        }
        $optSets[] = ' -o ' . escapeshellarg('media=Custom.' . $wmm . 'x' . $hmm . 'mm');
        $optSets[] = ' -o fit-to-page';
        $optSets[] = '';
        foreach ($optSets as $opts) {
            $lines = [];
            $code = 0;
            exec($prefix . escapeshellarg($lp) . ' -n 1' . $dest . $opts . ' ' . $file . ' 2>&1', $lines, $code);
            if ($code === 0) {
                return true;
            }
            $this->lastPrintError = trim(implode(' ', $lines));
        }
        $lines = [];
        $code = 0;
        exec($prefix . escapeshellarg($lpr) . $pdest . ' ' . $file . ' 2>&1', $lines, $code);
        if ($code === 0) {
            return true;
        }
        $extra = trim(implode(' ', $lines));
        if ($extra !== '') {
            $this->lastPrintError = trim($this->lastPrintError . ' ' . $extra);
        }

        return false;
    }

    private function printScript(): string
    {
        $candidates = [
            dirname(__DIR__) . '/bin/print-photo.ps1',
            dirname(__DIR__) . '/windows/print-photo.ps1',
        ];
        foreach ($candidates as $path) {
            if (is_file($path)) {
                return $path;
            }
        }

        return '';
    }

    /** @param array<string, mixed> $product */
    private function inchLabel(array $product): string
    {
        $sku = (string) ($product['sku'] ?? '');
        $known = [
            '10x15' => '4x6',
            '13x18' => '5x7',
            '15x21' => '6x8',
            '20x25' => '8x10',
            '20x30' => '8x12',
            '30x40' => '12x16',
            'id35x45' => '3.5x4.5',
            'passport' => '3.5x4.5',
            'id9x13' => '3.5x5',
            'notebook' => 'A4',
            'a4' => 'A4',
            'cal2027-1' => 'A4',
            'cal2027-12' => 'A4',
        ];
        if (isset($known[$sku])) {
            return $known[$sku];
        }
        $w = (float) ($product['width_cm'] ?? 0);
        $h = (float) ($product['height_cm'] ?? 0);
        if ($w <= 0 || $h <= 0) {
            return '';
        }

        return round($w / 2.54) . 'x' . round($h / 2.54);
    }

    /** @param array<string, array<string, mixed>> $products */
    private function copyDnpSizeAliases(string $exportDir, string $hotRoot, string $code, array $products): void
    {
        $job = [];
        $items = [];
        foreach ($products as $sku => $product) {
            $product['sku'] = $sku;
            $dir = $exportDir . DIRECTORY_SEPARATOR . $sku;
            if (! is_dir($dir)) {
                continue;
            }
            $inch = $this->inchLabel($product);
            if ($inch === '') {
                continue;
            }
            $alias = $hotRoot . DIRECTORY_SEPARATOR . $inch;
            if (! is_dir($alias) && ! mkdir($alias, 0775, true) && ! is_dir($alias)) {
                continue;
            }
            foreach (glob($dir . DIRECTORY_SEPARATOR . '*.jpg') ?: [] as $file) {
                $dest = $alias . DIRECTORY_SEPARATOR . $code . '_' . basename($file);
                copy($file, $dest);
                $items[] = $dest;
            }
        }
        unset($job, $items);
    }

    private function copyTree(string $src, string $dest): void
    {
        if (! is_dir($src)) {
            throw new \RuntimeException('Δεν υπάρχουν αρχεία εκτύπωσης.');
        }
        if (! is_dir($dest) && ! mkdir($dest, 0775, true) && ! is_dir($dest)) {
            throw new \RuntimeException('Δεν δημιουργήθηκε ο φάκελος αποθήκευσης: ' . $dest);
        }
        $iter = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($src, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );
        foreach ($iter as $file) {
            $rel = substr($file->getPathname(), strlen($src) + 1);
            $target = $dest . DIRECTORY_SEPARATOR . $rel;
            if ($file->isDir()) {
                if (! is_dir($target)) {
                    mkdir($target, 0775, true);
                }
                continue;
            }
            copy($file->getPathname(), $target);
        }
    }
}
