<?php

namespace App\Services;

use App\Models\Device;
use App\Models\SendLog;

class HdPlayerService
{
    private const CMD_HANDSHAKE    = 0x000B;
    private const CMD_MODE_INIT    = 0x0730;
    private const CMD_LOGIN        = 0x0410;
    private const CMD_GET_DEV_INFO = 0x000D;
    private const CMD_GET_STORAGE  = 0x040A;
    private const CMD_PRE_SEND     = 0x000F;
    private const CMD_QUERY_FILES  = 0x0011;
    private const CMD_QUERY_PROGS  = 0x0013;
    private const CMD_BEGIN_UPLOAD = 0x0015;
    private const CMD_DECLARE_FILE = 0x0017;
    private const CMD_FILE_DATA    = 0x0019;
    private const CMD_END_FILE     = 0x001B;
    private const CMD_ACTIVATE     = 0x001D;
    private const CMD_DONE         = 0x001F;

    private const CHUNK_SIZE       = 8192;
    private const CONNECT_TIMEOUT  = 10;
    private const READ_TIMEOUT     = 15;

    /** @var resource|null */
    private $sock = null;

    // ── Public send ──────────────────────────────────────────────────────────

    /**
     * Build image + .boo files and send to the device.
     * Returns a [success, message, duration_ms] tuple.
     */
    public static function sendMode(Device $device, string $mode): array
    {
        $start = microtime(true);
        try {
            $imagePath = storage_path("app/images/$mode.jpg");
            if (!file_exists($imagePath)) {
                throw new \RuntimeException("Image not found: $imagePath");
            }

            $imageData     = file_get_contents($imagePath);
            $imageMd5      = md5($imageData);
            $imageFilename = "$imageMd5.jpg";

            $booXml      = BooGenerator::generate(
                screenName: 'Screen1',
                widthPx:    $device->width_px,
                heightPx:   $device->height_px,
                imageMd5:   $imageMd5,
                imageExt:   'jpg',
                sceneName:  strtoupper($mode),
            );
            $booMd5      = md5($booXml);
            $booFilename = "$booMd5.boo";

            $svc = new self();
            $svc->connect($device->ip_address, $device->port);
            $svc->sendFiles([
                ['name' => $imageFilename, 'data' => $imageData],
                ['name' => $booFilename,   'data' => $booXml],
            ]);
            $svc->disconnect();

            $ms = (int) round((microtime(true) - $start) * 1000);
            return [true, 'Send complete', $ms];
        } catch (\Throwable $e) {
            $ms = (int) round((microtime(true) - $start) * 1000);
            return [false, $e->getMessage(), $ms];
        }
    }

    // ── TCP helpers ───────────────────────────────────────────────────────────

    private function connect(string $ip, int $port): void
    {
        $this->sock = @fsockopen($ip, $port, $errno, $errstr, self::CONNECT_TIMEOUT);
        if (!$this->sock) {
            throw new \RuntimeException("Cannot connect to $ip:$port — $errstr ($errno)");
        }
        stream_set_timeout($this->sock, self::READ_TIMEOUT);
    }

    private function disconnect(): void
    {
        if ($this->sock) { fclose($this->sock); $this->sock = null; }
    }

    private function sendFiles(array $files): void
    {
        // 1. Handshake
        $this->write($this->packet(self::CMD_HANDSHAKE, "\x09\x00\x00\x01"));
        $this->expect(self::CMD_HANDSHAKE + 1);

        // 2. ModeInit
        $this->write($this->packet(self::CMD_MODE_INIT, "\x02\x00\x00\x00\x00\x00\x00\x00"));
        $this->expect(self::CMD_MODE_INIT + 1);

        // 3. Login
        $this->write($this->packet(self::CMD_LOGIN, $this->loginString() . "\x00"));
        $this->expect(self::CMD_LOGIN + 1);

        // 4. GetDeviceInfo
        $this->write($this->packet(self::CMD_GET_DEV_INFO));
        $this->expect(self::CMD_GET_DEV_INFO + 1);

        // 5. GetStorage
        $this->write($this->packet(self::CMD_GET_STORAGE));
        $this->expect(self::CMD_GET_STORAGE + 1);

        // 6. PreSendInit
        $total = array_sum(array_map(fn($f) => strlen($f['data']), $files));
        $this->write($this->packet(self::CMD_PRE_SEND, pack('V', $total) . "\x00\x00\x00\x00"));
        $this->expect(self::CMD_PRE_SEND + 1);

        // 7. QueryFiles — drain until short response
        while (true) {
            $this->write($this->packet(self::CMD_QUERY_FILES));
            $resp = $this->read();
            if (strlen($resp) <= 5) break;
        }

        // 8. QueryPrograms
        $this->write($this->packet(self::CMD_QUERY_PROGS));
        $this->expect(self::CMD_QUERY_PROGS + 1);

        // 9. Upload each file
        foreach ($files as $file) {
            $name   = $file['name'];
            $data   = $file['data'];
            $offset = 0;
            $chunks = 0;

            $this->write($this->packet(self::CMD_BEGIN_UPLOAD, pack('V', 1)));
            $this->expect(self::CMD_BEGIN_UPLOAD + 1);

            $this->write($this->packet(self::CMD_DECLARE_FILE, $name . "\x00"));
            $this->expect(self::CMD_DECLARE_FILE + 1);

            while ($offset < strlen($data)) {
                $chunk = substr($data, $offset, self::CHUNK_SIZE);
                $this->write($this->packet(self::CMD_FILE_DATA, $chunk));
                $offset += strlen($chunk);
                $chunks++;
            }

            $this->write($this->packet(self::CMD_END_FILE));

            for ($i = 0; $i < $chunks; $i++) {
                $this->expect(self::CMD_FILE_DATA + 1);
            }
            $this->expect(self::CMD_END_FILE + 1);
        }

        // 10. Activate
        $this->write($this->packet(self::CMD_ACTIVATE));
        $this->expect(self::CMD_ACTIVATE + 1);

        // 11. Done
        $this->write($this->packet(self::CMD_DONE));
        $this->expect(self::CMD_DONE + 1);
    }

    private function packet(int $cmd, string $payload = ''): string
    {
        $total = 4 + strlen($payload);
        return pack('vv', $total, $cmd) . $payload;
    }

    private function write(string $data): void
    {
        $written = 0;
        $len     = strlen($data);
        while ($written < $len) {
            $n = fwrite($this->sock, substr($data, $written));
            if ($n === false) throw new \RuntimeException('Write failed');
            $written += $n;
        }
    }

    private function read(): string
    {
        $header = '';
        while (strlen($header) < 4) {
            $chunk = fread($this->sock, 4 - strlen($header));
            if ($chunk === false || $chunk === '') {
                $meta = stream_get_meta_data($this->sock);
                throw new \RuntimeException($meta['timed_out'] ? 'Read timeout' : 'Connection closed');
            }
            $header .= $chunk;
        }
        ['total' => $total] = unpack('vtotal/vcmd', $header);
        $remaining = $total - 4;
        $body = '';
        while ($remaining > 0) {
            $chunk = fread($this->sock, $remaining);
            if ($chunk === false || $chunk === '') throw new \RuntimeException('Connection closed mid-read');
            $body      .= $chunk;
            $remaining -= strlen($chunk);
        }
        return $header . $body;
    }

    private function expect(int $expectedCmd): string
    {
        $raw = $this->read();
        ['cmd' => $got] = unpack('vtotal/vcmd', $raw);
        if ($got !== $expectedCmd) {
            throw new \RuntimeException(sprintf(
                'Protocol error: expected cmd 0x%04X, got 0x%04X', $expectedCmd, $got
            ));
        }
        return $raw;
    }

    private function loginString(): string
    {
        $now = gmdate('Y-m-d_H:i:s');
        return implode(',', [
            'Windows', 'HDPlayer', 'Administrator', 'HDPANELMGR',
            'BFEBFBFF00000001',
            '00000000-0000-0000-0000-000000000001',
            'GA000UV_HD000001',
            $now,
            'ethernet_32774-0.0.0.0-00:00:00:00:00:00',
        ]);
    }
}
