<?php

namespace App\Support\LegacyCrypto;

use RuntimeException;

class CiSeedCrypto
{
    private array $userKeyBytes; // int[16] signed bytes
    private array $iv;           // int[16]
    private string $serverEncoding;
    private string $innerEncoding;
    private int $block;

    public function __construct(string $userKey, array $iv)
    {
        $this->serverEncoding = config('legacy.server_encoding', 'UTF-8');
        $this->innerEncoding  = config('legacy.inner_encoding', 'UTF-8');
        $this->block          = (int) config('legacy.block', 16);

        if (count($iv) !== 16) {
            throw new RuntimeException('legacy.iv must be 16 bytes');
        }
        $this->iv = array_values($iv);

        // 핵심: CI SeedRoundKey는 pbUserKey[0..15]만 사용 → 문자열 키의 "앞 16바이트"만 의미 있음
        $this->userKeyBytes = $this->stringKeyToSigned16Bytes($userKey);
    }

    public function encrypt(string $plain): string
    {
        $plain = $this->convertEncoding($plain);
        if ($plain === '') return $plain;

        $planBytes = $this->unpackSignedBytes($plain);

        $seed = new Seed();
        $pdwRoundKey = null;
        $seed->SeedRoundKey($pdwRoundKey, $this->userKeyBytes);

        $planLen = count($planBytes);
        $start = 0;
        $end = 0;

        $cbcBlock = [];
        $this->arrayCopy($this->iv, 0, $cbcBlock, 0, $this->block);

        $ret = '';

        while ($end < $planLen) {
            $end = $start + $this->block;
            if ($end > $planLen) $end = $planLen;

            $cipherBlock = [];
            $this->arrayCopy($planBytes, $start, $cipherBlock, 0, $end - $start);

            // PKCS#5 padding
            $nPad = $this->block - ($end - $start);
            for ($i = ($end - $start); $i < $this->block; $i++) {
                $cipherBlock[$i] = $nPad;
            }

            // CBC XOR
            $this->xor16($cipherBlock, $cbcBlock, $cipherBlock);

            // Encrypt
            $encBlock = null;
            $seed->SeedEncrypt($cipherBlock, $pdwRoundKey, $encBlock);

            // CBC 갱신
            $this->arrayCopy($encBlock, 0, $cbcBlock, 0, $this->block);

            foreach ($encBlock as $b) {
                $ret .= bin2hex(chr($b & 0xFF));
            }

            $start = $end;
        }

        return $ret;
    }

    public function decrypt(string $cipherHex): string
    {
        $cipherHex = trim($cipherHex);
        if ($cipherHex === '') return '';

        if (strlen($cipherHex) % 32 !== 0) {
            throw new RuntimeException('Invalid cipher hex length (must be multiple of 32)');
        }

        $seed = new Seed();
        $pdwRoundKey = null;
        $seed->SeedRoundKey($pdwRoundKey, $this->userKeyBytes);

        $cbcBlock = [];
        $this->arrayCopy($this->iv, 0, $cbcBlock, 0, $this->block);

        $plainBytes = [];
        $blocks = (int)(strlen($cipherHex) / 32);

        for ($bi = 0; $bi < $blocks; $bi++) {
            $hexBlock = substr($cipherHex, $bi * 32, 32);
            $cipherBlock = $this->hexToBytesSigned($hexBlock); // signed 16 bytes

            $decBlock = null;
            $seed->SeedDecrypt($cipherBlock, $pdwRoundKey, $decBlock);

            $plainBlock = [];
            $this->xor16($decBlock, $cbcBlock, $plainBlock);

            // CBC 갱신
            $this->arrayCopy($cipherBlock, 0, $cbcBlock, 0, $this->block);

            foreach ($plainBlock as $b) {
                $plainBytes[] = $b;
            }
        }

        $plainBytes = $this->pkcs5Unpad($plainBytes);
        $plain = $this->packSignedBytes($plainBytes);

        return $this->convertEncodingBack($plain);
    }

    /* -------------------- KEY NORMALIZE (핵심) -------------------- */

    private function stringKeyToSigned16Bytes(string $key): array
    {
        // 레거시(운영 CI)에서 "문자열을 배열처럼 접근 + & 연산"하던 동작을 최대한 재현
        // 문자 1개를 (int)로 캐스팅: 숫자면 0~9, 숫자 아니면 0
        $bytes = [];

        for ($i = 0; $i < 16; $i++) {
            $ch = $key[$i] ?? "\0";   // 1-char string
            $v  = (int) $ch;          // 핵심: ord()가 아니라 int 캐스팅
            // signed byte 범위 맞추기(사실 0~9라 필요없지만 안전)
            if ($v > 127) $v -= 256;
            $bytes[] = $v;
        }

        return $bytes;
    }

    /* -------------------- UTIL -------------------- */

    private function convertEncoding(string $s): string
    {
        $out = @iconv($this->serverEncoding, $this->innerEncoding, $s);
        return $out === false ? $s : $out;
    }

    private function convertEncodingBack(string $s): string
    {
        $out = @iconv($this->innerEncoding, $this->serverEncoding, $s);
        return $out === false ? $s : $out;
    }

    private function unpackSignedBytes(string $s): array
    {
        return array_values(unpack('c*', $s));
    }

    private function packSignedBytes(array $bytes): string
    {
        $out = '';
        foreach ($bytes as $b) {
            $out .= chr($b & 0xFF);
        }
        return $out;
    }

    private function hexToBytesSigned(string $hexBlock): array
    {
        $bin = hex2bin($hexBlock);
        if ($bin === false || strlen($bin) !== 16) {
            throw new RuntimeException('Invalid hex block');
        }
        return $this->unpackSignedBytes($bin);
    }

    private function pkcs5Unpad(array $bytes): array
    {
        $n = count($bytes);
        if ($n === 0) return $bytes;

        $pad = $bytes[$n - 1] & 0xFF;
        if ($pad < 1 || $pad > 16) return $bytes;

        for ($i = 0; $i < $pad; $i++) {
            if (($bytes[$n - 1 - $i] & 0xFF) !== $pad) {
                return $bytes;
            }
        }
        return array_slice($bytes, 0, $n - $pad);
    }

    private function xor16(array $a, array $b, array &$out): void
    {
        for ($i = 0; $i < 16; $i++) {
            $out[$i] = ($a[$i] ^ $b[$i]);
        }
    }

    private function arrayCopy(array $src, int $srcPos, array &$dst, int $dstPos, int $length): void
    {
        for ($i = 0; $i < $length; $i++) {
            $dst[$dstPos + $i] = $src[$srcPos + $i];
        }
    }
}
