package com.pjpmosteiro.itartanas.nfc

import android.nfc.Tag
import android.nfc.tech.IsoDep
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*

data class CardData(
    val id: String,
    val balance: Double? = null,
    val lastOperation: String? = null
)

class CardRepositoryImpl(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
    
    suspend fun getCard(tag: Tag): Result<CardData> = withContext(dispatcher) {
        try {
            val isoDep = IsoDep.get(tag)
            isoDep.connect()
            
            // Seleccionar red de transporte
            val transportNetworkResponse = selectTransportNetwork(isoDep)
            
            // Leer entorno
            readEnvironment(isoDep)
            
            // Leer contrato
            val contract = readContract(isoDep)
            
            // Obtener ID de tarjeta
            val cardId = getCardId(transportNetworkResponse)
            
            // Obtener saldo (bytes 29-31, 3 bytes, big endian, en céntimos)
            val balance = toIntBigEndian(contract, 29, 3) / 100.0
            
            // Intentar extraer información de última validación del contrato
            val lastValidation = try {
                extractLastValidation(contract)
            } catch (e: Exception) {
                null
            }
            
            isoDep.close()
            
            Result.success(CardData(
                id = cardId, 
                balance = balance,
                lastOperation = lastValidation
            ))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    private fun getCardId(transportNetworkResponse: ByteArray): String {
        val bArrSliceArray = transportNetworkResponse.sliceArray(2 until transportNetworkResponse.size - 2)
        var strBcdToDecString = bcdToDecString(bArrSliceArray)
        
        val strSubstring = strBcdToDecString.substring(6, 8)
        val str = when {
            strSubstring == "04" || !strBcdToDecString.startsWith("987020") -> {
                if (strSubstring != "04") "00" else null
            }
            else -> "01"
        }
        
        if (str != null) {
            val strPanToIep = panToIep(bArrSliceArray, str)
            if (strPanToIep != null) {
                strBcdToDecString = strPanToIep
            }
        }
        
        return groupEvery(maskFirstNChars(strBcdToDecString, 8), 4)
    }
    
    private fun selectTransportNetwork(isoDep: IsoDep): ByteArray {
        return sendApdu(isoDep, byteArrayOf(0x80.toByte(), 0x26, 0x4F, 0x11, 0x0A), 12)
    }
    
    private fun readEnvironment(isoDep: IsoDep): ByteArray {
        return sendApdu(isoDep, byteArrayOf(0x80.toByte(), 0x32, 0x00, 0x00, 0x18), 26)
    }
    
    private fun readContract(isoDep: IsoDep): ByteArray {
        return sendApdu(isoDep, byteArrayOf(0x80.toByte(), 0x2E, 0x01, 0x00, 0x20), 34)
    }
    
    private fun extractLastValidation(contract: ByteArray): String? {
        return try {
            // El contrato tiene 32 bytes de datos útiles (34 total con SW)
            // Intentar extraer fecha de última validación si está disponible
            
            // Bytes 8-9: posible fecha de última validación (días desde epoch Calypso)
            if (contract.size >= 11) {
                val days = toIntBigEndian(contract, 8, 2)
                if (days > 0 && days < 20000) { // Validación: entre 1997 y ~2050
                    val epochDate = Calendar.getInstance().apply {
                        set(1997, Calendar.JANUARY, 1, 0, 0, 0)
                        set(Calendar.MILLISECOND, 0)
                        add(Calendar.DAY_OF_YEAR, days)
                    }
                    val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
                    return "Última validación: ${dateFormat.format(epochDate.time)}"
                }
            }
            null
        } catch (e: Exception) {
            null
        }
    }
    
    private fun sendApdu(isoDep: IsoDep, command: ByteArray, responseSize: Int, statusWord: String = "9000"): ByteArray {
        val response = isoDep.transceive(command)
        
        // Verificar status word primero
        if (response.size >= 2) {
            val sw = String.format("%02X%02X", response[response.size - 2], response[response.size - 1])
            // 6A83 = Record not found (es normal para registros vacíos)
            if (sw == "6A83" || sw == "6A82") {
                throw IllegalStateException("Registro no encontrado")
            }
            if (sw != statusWord) {
                throw IllegalArgumentException("APDU error: expected status word $statusWord but was $sw")
            }
        }
        
        // El tamaño puede variar ligeramente, ser flexible
        if (response.size < responseSize - 2 || response.size > responseSize + 2) {
            throw IllegalArgumentException("APDU response size error: expected ~$responseSize bytes but was ${response.size}")
        }
        
        return response
    }
    
    private fun requireSize(response: ByteArray, size: Int) {
        if (response.size != size) {
            throw IllegalArgumentException("APDU response size error: expected $size bytes but was ${response.size}")
        }
    }
    
    private fun requireStatusWord(response: ByteArray, statusWord: String) {
        val sw = String.format("%02X%02X", response[response.size - 2], response[response.size - 1])
        if (statusWord != sw) {
            throw IllegalArgumentException("APDU error: expected status word $statusWord but was $sw")
        }
    }
    
    private fun toIntBigEndian(arr: ByteArray, offset: Int, length: Int): Int {
        if (offset < 0 || offset + length > arr.size) {
            throw IllegalArgumentException("Array too small: need $length bytes at offset $offset but size is ${arr.size}")
        }
        var result = 0
        for (i in 0 until length) {
            result = (result shl 8) or (arr[offset + i].toInt() and 0xFF)
        }
        return result
    }
    
    private fun bcdToDecString(arr: ByteArray): String {
        if (arr.size != 8) {
            throw IllegalArgumentException("8 BCD bytes were expected from PAN, but they were ${arr.size}")
        }
        return arr.joinToString("") { byte ->
            val i = byte.toInt() and 0xFF
            val high = i ushr 4
            val low = byte.toInt() and 0x0F
            if (high >= 10 || low >= 10) {
                throw IllegalArgumentException("Byte is not valid BCD: ${String.format("%02X", i)}")
            }
            "$high$low"
        }
    }
    
    private fun panToIep(arr: ByteArray, apli: String): String? {
        if (apli.length != 2) {
            throw IllegalArgumentException("Apli must have exactly 2 characters")
        }
        val pan16 = bcdToDecString(arr).padEnd(16, '0')
        val result = pan16.substring(0, 6) + apli + pan16.substring(8, 15) + "0"
        val partial = result.substring(0, 15)
        return partial + luhnCheckDigit(result)
    }
    
    private fun luhnCheckDigit(str: String): Char {
        var sum = 0
        var i = 0
        str.forEach { char ->
            i++
            var digit = char.digitToInt()
            if (i % 2 == (str.length + 1) % 2) {
                digit *= 2
            }
            if (digit > 9) {
                digit -= 9
            }
            sum += digit
        }
        return ((10 - (sum % 10)) % 10).digitToChar()
    }
    
    private fun maskFirstNChars(str: String, n: Int): String {
        if (str.length <= n) {
            return "*".repeat(str.length)
        }
        return "*".repeat(n) + str.substring(n)
    }
    
    private fun groupEvery(str: String, n: Int, separator: String = " "): String {
        return str.chunked(n).joinToString(separator)
    }
}

