<?php

declare(strict_types=1);

namespace App\Http\Controllers\Helpers;

/**
 * Decoder for QRPH / EMV QRCPS strings.
 *
 * QRPH follows the EMVCo Merchant-Presented QR Code Specification.
 * Each field is encoded as: 2-char tag + 2-char length (decimal) + value.
 * Nested (sub-field) structures use the same encoding inside a parent value.
 */
class QrphDecoder
{
    /**
     * Decode a QRPH string and return human-readable fields.
     */
    public static function decode(string $qrString): array
    {
        $qrString = trim($qrString);
        $fields = self::parseTlv($qrString);

        return [
            'raw'              => $qrString,
            'valid'            => self::validateCrc($qrString, $fields),
            'initiation_method'=> self::initiationLabel($fields['01'] ?? null),
            'merchant_name'    => $fields['59'] ?? null,
            'merchant_city'    => $fields['60'] ?? null,
            'country_code'     => $fields['58'] ?? null,
            'currency'         => $fields['53'] ?? null,  // 608 = PHP
            'amount'           => isset($fields['54']) ? (float) $fields['54'] : null,
            'merchant_account' => self::extractMerchantAccount($fields),
        ];
    }

    /**
     * Parse a flat TLV string into [ tag => value ] pairs.
     */
    public static function parseTlv(string $data): array
    {
        $result = [];
        $pos    = 0;
        $len    = strlen($data);

        while ($pos + 4 <= $len) {
            $tag    = substr($data, $pos, 2);
            $length = (int) substr($data, $pos + 2, 2);
            $value  = substr($data, $pos + 4, $length);

            $result[$tag] = $value;
            $pos += 4 + $length;

            if ($tag === '63') break; // CRC is always last
        }

        return $result;
    }

    /**
     * Extract primary account info from merchant account fields (tags 26–51).
     * Returns the first one found with a recognisable GUID.
     */
    private static function extractMerchantAccount(array $fields): ?array
    {
        for ($i = 26; $i <= 51; $i++) {
            $tag = str_pad((string) $i, 2, '0', STR_PAD_LEFT);
            if (!isset($fields[$tag])) continue;

            $sub  = self::parseTlv($fields[$tag]);
            $guid = $sub['00'] ?? '';

            return [
                'guid'    => $guid,
                'network' => self::networkFromGuid($guid),
                'account' => $sub['01'] ?? null,
                'name'    => $sub['02'] ?? null,
            ];
        }
        return null;
    }

    private static function networkFromGuid(string $guid): string
    {
        $map = [
            // Standard QRPH / InstaPay (BSP / PPMI)
            'com.p2pqrph'              => 'InstaPay (QRPH)',
            'ph.ppmi'                  => 'InstaPay',
            'ph.bsp.qrph'              => 'InstaPay (BSP)',
            // GoTyme Bank
            'com.gotyme'               => 'GoTyme',
            'ph.gotyme'                => 'GoTyme',
            'gotyme'                   => 'GoTyme',
            // GCash
            'com.gcash'                => 'GCash',
            // Maya / PayMaya
            'com.paymaya'              => 'Maya',
            'com.maya'                 => 'Maya',
            // Major PH banks
            'ph.bpi'                   => 'BPI',
            'ph.bdo'                   => 'BDO',
            'com.landbank'             => 'Landbank',
            'ph.landbank'              => 'Landbank',
            'ph.ubp'                   => 'UnionBank',
            'ph.metrobank'             => 'Metrobank',
            'ph.rcbc'                  => 'RCBC',
            'ph.pnb'                   => 'PNB',
            'ph.securitybank'          => 'Security Bank',
            'ph.chinabank'             => 'China Bank',
            'ph.eastwest'              => 'EastWest Bank',
            'ph.psbank'                => 'PSBank',
            'ph.boc'                   => 'Bank of Commerce',
            'com.seabank'              => 'SeaBank',
            'com.tonik'                => 'Tonik',
            'com.uno'                  => 'UNO Digital Bank',
            'com.komo'                 => 'CIMB / Komo',
            'com.ownbank'              => 'OwnBank',
        ];

        $lower = strtolower($guid);
        foreach ($map as $key => $label) {
            if (str_contains($lower, $key)) return $label;
        }

        // Fallback: return the raw GUID so it's still visible
        return $guid ?: 'Unknown';
    }

    private static function initiationLabel(?string $code): string
    {
        return match ($code) {
            '11'    => 'Static',
            '12'    => 'Dynamic',
            default => $code ?? 'Unknown',
        };
    }

    /**
     * Validate the CRC-16/CCITT-FALSE checksum embedded in the QR string.
     */
    public static function validateCrc(string $qrString, array $parsed = []): bool
    {
        if (empty($parsed['63'])) return false;

        // CRC covers everything up to and including the "6304" tag+length prefix.
        $crcPos = strrpos($qrString, '6304');
        if ($crcPos === false) return false;

        $payload  = substr($qrString, 0, $crcPos + 4);
        $expected = strtoupper($parsed['63']);
        $computed = self::crc16($payload);

        return $computed === $expected;
    }

    /**
     * CRC-16/CCITT-FALSE (polynomial 0x1021, seed 0xFFFF).
     */
    public static function crc16(string $data): string
    {
        $crc = 0xFFFF;
        for ($i = 0, $l = strlen($data); $i < $l; $i++) {
            $crc ^= (ord($data[$i]) << 8);
            for ($j = 0; $j < 8; $j++) {
                $crc = ($crc & 0x8000)
                    ? (($crc << 1) ^ 0x1021) & 0xFFFF
                    : ($crc << 1) & 0xFFFF;
            }
        }
        return sprintf('%04X', $crc);
    }
}
