package com.walkguide.service;

import com.walkguide.dto.response.AgoraTokenResponse;
import com.walkguide.entity.User;
import com.walkguide.exception.ResourceNotFoundException;
import com.walkguide.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Instant;
import java.util.Base64;
import java.util.zip.CRC32;

/**
 * Service untuk generate Agora RTC Access Token versi 006.
 *
 * Implementasi pure Java dari Agora Token Builder:
 * https://github.com/AgoraIO/Tools/tree/master/DynamicKey/AgoraDynamicKey/java
 *
 * TIDAK perlu library eksternal — hanya pakai javax.crypto (built-in JDK).
 *
 * Token format: {appId}006{base64EncodedPack}
 * Pack berisi: version, appId, appCertificate, channelName, uid, privileges, timestamp, salt
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class AgoraTokenService {

    private final UserRepository userRepository;

    @Value("${agora.app-id:}")
    private String agoraAppId;

    @Value("${agora.app-certificate:}")
    private String agoraAppCertificate;

    /** Durasi token: 1 jam (3600 detik) */
    private static final int TOKEN_EXPIRE_SECONDS = 3600;

    /** Privilege values dari Agora spec */
    private static final short PRIVILEGE_JOIN_CHANNEL         = 1;
    private static final short PRIVILEGE_PUBLISH_AUDIO_STREAM = 2;
    private static final short PRIVILEGE_PUBLISH_VIDEO_STREAM = 3;
    private static final short PRIVILEGE_PUBLISH_DATA_STREAM  = 4;

    /**
     * Generate Agora token untuk VoIP call antara 2 user.
     *
     * Channel name dibuat deterministik dari pair ID:
     *   "call_{min(id1,id2)}_{max(id1,id2)}"
     * Sehingga kedua pihak selalu join channel yang sama.
     *
     * @param callerId   userId yang inisiasi call
     * @param receiverId userId yang menerima call
     * @return AgoraTokenResponse berisi token, channelName, uid, appId, expiresAt
     */
    public AgoraTokenResponse generateToken(Long callerId, Long receiverId) {
        // Validasi kedua user ada
        User caller = userRepository.findById(callerId)
                .orElseThrow(() -> new ResourceNotFoundException("Caller not found"));
        userRepository.findById(receiverId)
                .orElseThrow(() -> new ResourceNotFoundException("Receiver not found"));

        // Channel name deterministik — kedua pihak join channel yang sama
        long minId = Math.min(callerId, receiverId);
        long maxId = Math.max(callerId, receiverId);
        String channelName = String.format("call_%d_%d", minId, maxId);

        // UID untuk caller (bisa pakai int dari userId, mod jika > max int)
        int uid = (int) (callerId % Integer.MAX_VALUE);

        long expiresAt = Instant.now().getEpochSecond() + TOKEN_EXPIRE_SECONDS;

        String token;
        if (agoraAppId.isBlank() || agoraAppCertificate.isBlank()) {
            // Mode demo: token kosong — Agora masih bisa connect tanpa token
            // (hanya di Agora project mode "Testing", bukan "Live")
            token = "";
            log.warn("[AGORA] App ID/Certificate belum dikonfigurasi. Token mode demo (kosong). " +
                     "Set agora.app-id dan agora.app-certificate di application.properties");
        } else {
            token = buildToken006(agoraAppId, agoraAppCertificate, channelName, uid, expiresAt);
            log.info("[AGORA] Token generated untuk channel={} uid={} expires={}", channelName, uid, expiresAt);
        }

        return AgoraTokenResponse.builder()
                .token(token)
                .channelName(channelName)
                .uid(uid)
                .appId(agoraAppId)
                .expiresAt(expiresAt)
                .callerName(caller.getDisplayName())
                .build();
    }

    // ===========================
    // AGORA TOKEN 006 BUILDER
    // Pure Java — mirip implementasi resmi Agora
    // ===========================

    private String buildToken006(String appId, String appCertificate,
                                  String channelName, int uid, long expiresAt) {
        try {
            int salt = (int) (Math.random() * Integer.MAX_VALUE);
            int timestamp = (int) (expiresAt);

            // Privileges: JOIN_CHANNEL berlaku 1 jam, PUBLISH juga 1 jam
            byte[] packContent = packPrivileges(salt, timestamp, uid,
                    PRIVILEGE_JOIN_CHANNEL,          TOKEN_EXPIRE_SECONDS,
                    PRIVILEGE_PUBLISH_AUDIO_STREAM,  TOKEN_EXPIRE_SECONDS,
                    PRIVILEGE_PUBLISH_VIDEO_STREAM,  TOKEN_EXPIRE_SECONDS,
                    PRIVILEGE_PUBLISH_DATA_STREAM,   TOKEN_EXPIRE_SECONDS);

            // Signing content: appId + appCertificate + channelName + uid + privileges
            byte[] signing = buildSigningContent(appId, appCertificate, channelName,
                    uid, packContent);

            // HMAC-SHA256 signature
            byte[] signature = hmacSha256(appCertificate.getBytes(), signing);

            // Assemble final token pack
            byte[] tokenPack = assembleTokenPack(appId, signature, packContent, channelName, uid);

            return "006" + appId + Base64.getEncoder().encodeToString(tokenPack);

        } catch (Exception e) {
            log.error("[AGORA] Token build error: {}", e.getMessage());
            return "";
        }
    }

    private byte[] packPrivileges(int salt, int timestamp, int uid,
                                   Object... privilegePairs) throws Exception {
        // Count privileges
        int numPrivileges = privilegePairs.length / 2;

        // Estimate buffer size
        ByteBuffer buf = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN);

        buf.putShort((short) 1);        // version
        buf.putInt(salt);
        buf.putInt(timestamp);

        // Privileges map: count + [key, value] pairs
        buf.putShort((short) numPrivileges);
        for (int i = 0; i < privilegePairs.length; i += 2) {
            short key = ((Number) privilegePairs[i]).shortValue();
            int   val = ((Number) privilegePairs[i + 1]).intValue();
            buf.putShort(key);
            buf.putInt(val);
        }

        byte[] result = new byte[buf.position()];
        buf.flip();
        buf.get(result);
        return result;
    }

    private byte[] buildSigningContent(String appId, String appCertificate,
                                        String channelName, int uid,
                                        byte[] packContent) {
        byte[] appIdBytes        = appId.getBytes();
        byte[] channelNameBytes  = channelName.getBytes();
        String uidStr            = uid == 0 ? "" : String.valueOf(uid);
        byte[] uidBytes          = uidStr.getBytes();

        // CRC32 of channel name and uid for signing
        CRC32 crc32 = new CRC32();
        crc32.update(channelNameBytes);
        long crcChannel = crc32.getValue();

        crc32.reset();
        crc32.update(uidBytes);
        long crcUid = crc32.getValue();

        ByteBuffer buf = ByteBuffer.allocate(
                appIdBytes.length + packContent.length + 4 + 4
        ).order(ByteOrder.LITTLE_ENDIAN);

        buf.put(appIdBytes);
        buf.put(packContent);
        buf.putInt((int) crcChannel);
        buf.putInt((int) crcUid);

        byte[] result = new byte[buf.position()];
        buf.flip();
        buf.get(result);
        return result;
    }

    private byte[] hmacSha256(byte[] key, byte[] data) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return mac.doFinal(data);
    }

    private byte[] assembleTokenPack(String appId, byte[] signature,
                                      byte[] packContent, String channelName, int uid) {
        byte[] sigBytes      = signature;
        byte[] appIdBytes    = appId.getBytes();
        byte[] channelBytes  = channelName.getBytes();
        String uidStr        = uid == 0 ? "" : String.valueOf(uid);
        byte[] uidBytes      = uidStr.getBytes();

        // CRC32 of channel and uid
        CRC32 crc32 = new CRC32();
        crc32.update(channelBytes);
        long crcChannel = crc32.getValue();

        crc32.reset();
        crc32.update(uidBytes);
        long crcUid = crc32.getValue();

        // Pack: [sigLen][sig][crcChannel][crcUid][packContentLen][packContent]
        ByteBuffer buf = ByteBuffer.allocate(
                2 + sigBytes.length + 4 + 4 + 2 + packContent.length
        ).order(ByteOrder.LITTLE_ENDIAN);

        buf.putShort((short) sigBytes.length);
        buf.put(sigBytes);
        buf.putInt((int) crcChannel);
        buf.putInt((int) crcUid);
        buf.putShort((short) packContent.length);
        buf.put(packContent);

        byte[] result = new byte[buf.position()];
        buf.flip();
        buf.get(result);
        return result;
    }
}
