//
//  BcaFlazzApi.swift
//  Emoney Info
//
//  Created by Wira Irawan on 27/07/24.
//

import Foundation
import CoreNFC

public class BcaFlazzApi : UnifiedNfcApi {
    private let emoney: Emoney = Emoney()
    private var ef84Records: [RiwayatCard] = []
    private var extendedRecords: [RiwayatCard] = []
    private var historyRetryCount = 0

    private let ef84MaxSlots = 10
    private let extendedMaxRecords = 256

    public override init() {}

    public func checkFlazzCard() {
        selectDirectoryFile(useFallbackLe: false)
    }

    private func selectDirectoryFile(useFallbackLe: Bool) {
        let command = NFCISO7816APDU(
            instructionClass: 0x00,
            instructionCode: 0xA4,
            p1Parameter: 0x01,
            p2Parameter: 0x00,
            data: Data([0x02, 0x00]),
            expectedResponseLength: useFallbackLe ? 256 : -1
        )

        apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
            if response.sw1 == 0x90 && response.sw2 == 0x00 {
                self.emoney.setCardLabel("BCA Flazz")
                self.getCardNumber()
            } else if !useFallbackLe {
                self.selectDirectoryFile(useFallbackLe: true)
            } else {
                self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
            }
        })
    }

    private func getCardNumber() {
        apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU02, completionHandler: { response in
            if response.sw1 == 0x90 && response.sw2 == 0x00 {
                guard let raw = String(data: response.getData(), encoding: .isoLatin1) else {
                    self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
                    return
                }

                if let cardNumber = self.extractPan(from: raw) {
                    self.emoney.setCardNumber(cardNumber)
                    self.getBalance()
                } else {
                    self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
                }
            } else {
                self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
            }
        })
    }

    private func getBalance() {
        apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU03, completionHandler: { response in
            if response.sw1 == 0x90 && response.sw2 == 0x00 {
                let bytes = response.getData().bytes
                guard bytes.count >= 4 else {
                    self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
                    return
                }

                self.emoney.setBalance(self.uint32BE(bytes, offset: 0))
                self.historyRetryCount = 0
                self.resetHistoryState()
                self.readHistoryCheck()
            } else {
                self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
            }
        })
    }

    private func resetHistoryState() {
        ef84Records.removeAll()
        extendedRecords.removeAll()
    }

    private func retryHistoryRead(reason: String) {
        if historyRetryCount < 1 {
            historyRetryCount += 1
            debugLog("Retrying Flazz history read: \(reason)")
            resetHistoryState()
            readHistoryCheck()
        } else {
            finishReading()
        }
    }

    private func readHistoryCheck() {
        apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU04, completionHandler: { response in
            let shouldReadExtended = response.sw1 == 0x90 && response.sw2 == 0x00
            self.readEf84Record(slot: 0, shouldReadExtended: shouldReadExtended)
        })
    }

    private func readEf84Record(slot: Int, shouldReadExtended: Bool) {
        guard slot < ef84MaxSlots else {
            if shouldReadExtended {
                getChallenge()
            } else {
                finishReading()
            }
            return
        }

        let command = NFCISO7816APDU(
            instructionClass: 0x00,
            instructionCode: 0xB0,
            p1Parameter: 0x84,
            p2Parameter: UInt8(slot * 0x0F),
            data: Data(),
            expectedResponseLength: 60
        )

        apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
            if response.sw1 == 0x90 && response.sw2 == 0x00 {
                if let record = self.parseEf84Record(response.getData()) {
                    self.ef84Records.append(record)
                }
                self.readEf84Record(slot: slot + 1, shouldReadExtended: shouldReadExtended)
            } else if response.sw1 == 0x6B && response.sw2 == 0x00 {
                if shouldReadExtended {
                    self.getChallenge()
                } else {
                    self.finishReading()
                }
            } else {
                self.retryHistoryRead(reason: "EF84 status \(response.sw1)-\(response.sw2) at slot \(slot)")
            }
        })
    }

    private func getChallenge() {
        let command = NFCISO7816APDU(
            instructionClass: 0x00,
            instructionCode: 0x84,
            p1Parameter: 0x00,
            p2Parameter: 0x00,
            data: Data(),
            expectedResponseLength: 8
        )

        apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
            if response.sw1 == 0x90 && response.sw2 == 0x00 {
                self.readExtendedRecord(index: 0)
            } else {
                self.retryHistoryRead(reason: "challenge status \(response.sw1)-\(response.sw2)")
            }
        })
    }

    private func readExtendedRecord(index: Int) {
        guard index < extendedMaxRecords else {
            finishReading()
            return
        }

        let command = NFCISO7816APDU(
            instructionClass: 0x90,
            instructionCode: 0x32,
            p1Parameter: 0x03,
            p2Parameter: 0x00,
            data: Data([UInt8(index)]),
            expectedResponseLength: 32
        )

        apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
            if response.sw1 == 0x90 && response.sw2 == 0x00 {
                if let record = self.parseExtendedRecord(response.getData()) {
                    self.extendedRecords.append(record)
                }
                self.readExtendedRecord(index: index + 1)
            } else if response.sw1 == 0x6A && response.sw2 == 0x80 {
                self.finishReading()
            } else {
                self.retryHistoryRead(reason: "extended status \(response.sw1)-\(response.sw2) at index \(index)")
            }
        })
    }

    private func finishReading() {
        var history = ef84Records + extendedRecords
        history.sort {
            ($0.getTransationTime() ?? Date.distantPast) > ($1.getTransationTime() ?? Date.distantPast)
        }

        if !history.isEmpty {
            emoney.setRiwayatList(history)
            emoney.setTampilRiwayat(true)
        }

        updateScreen()
        apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
        apduRunner.invalidateSession()
    }

    private func parseEf84Record(_ data: Data) -> RiwayatCard? {
        let bytes = data.bytes
        guard bytes.count >= 60 else {
            return nil
        }

        let item = RiwayatCard()
        let typeRaw = (Int(bytes[0]) << 8) | Int(bytes[1])
        let amount = uint24BE(bytes, offset: 6)
        let transactionTime = uint32BE(bytes, offset: 38)
        let terminalId = asciiString(bytes, start: 30, length: 8)
        let isPayment = typeRaw == 0x0400

        item.setAmount(amount)
        item.setLocationId(terminalId)
        item.setLocationName(isPayment ? "payment".localizeString(string: self.langCode!) : "topup".localizeString(string: self.langCode!))
        item.setTransactionTime(formatFlazzTimestamp(seconds: transactionTime))

        if isPayment {
            item.setProsesTipe(1)
            item.setTitle("payment".localizeString(string: self.langCode!))
        } else {
            item.setProsesTipe(0)
            item.setTitle("topup".localizeString(string: self.langCode!))
        }

        return transactionTime > 0 ? item : nil
    }

    private func parseExtendedRecord(_ data: Data) -> RiwayatCard? {
        let bytes = data.bytes
        guard bytes.count >= 32 else {
            return nil
        }

        let item = RiwayatCard()
        let type = bytes[0]
        let rawTimestamp = Array(bytes[4..<8]).hexString().uppercased()
        let timestampSeconds = uint32BE(bytes, offset: 4)
        let rawAmount = uint24BE(bytes, offset: 1)
        let terminalId = asciiString(bytes, start: 14, length: 8)
        let isPayment = type == 0x04
        let amount = isPayment ? 0x1000000 - rawAmount : rawAmount

        item.setAmount(amount)
        item.setLocationId(terminalId)
        item.setLocationName(isPayment ? "payment".localizeString(string: self.langCode!) : "topup".localizeString(string: self.langCode!))
        item.setDesk("RAW TS: \(rawTimestamp)")
        item.setTransactionTime(formatFlazzTimestamp(seconds: timestampSeconds))

        if isPayment {
            item.setProsesTipe(1)
            item.setTitle("payment".localizeString(string: self.langCode!))
        } else {
            item.setProsesTipe(0)
            item.setTitle("topup".localizeString(string: self.langCode!))
        }

        return timestampSeconds > 0 ? item : nil
    }

    private func extractPan(from raw: String) -> String? {
        guard let start = raw.firstIndex(of: ";") else {
            return nil
        }

        let tail = raw[raw.index(after: start)...]
        guard let end = tail.firstIndex(of: "=") else {
            return nil
        }

        return String(tail[..<end])
    }

    private func uint24BE(_ bytes: [UInt8], offset: Int) -> Int {
        guard bytes.count >= offset + 3 else {
            return 0
        }

        return (Int(bytes[offset]) << 16)
            | (Int(bytes[offset + 1]) << 8)
            | Int(bytes[offset + 2])
    }

    private func uint32BE(_ bytes: [UInt8], offset: Int) -> Int {
        guard bytes.count >= offset + 4 else {
            return 0
        }

        return (Int(bytes[offset]) << 24)
            | (Int(bytes[offset + 1]) << 16)
            | (Int(bytes[offset + 2]) << 8)
            | Int(bytes[offset + 3])
    }

    private func formatFlazzTimestamp(seconds: Int) -> Date {
        var dateComponents = DateComponents()
        dateComponents.year = 1980
        dateComponents.month = 1
        dateComponents.day = 1
        dateComponents.timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
        dateComponents.hour = 0
        dateComponents.minute = 0
        dateComponents.second = seconds
        return Calendar.current.date(from: dateComponents) ?? Date.distantPast
    }

    private func asciiString(_ bytes: [UInt8], start: Int, length: Int) -> String {
        guard bytes.count >= start + length else {
            return ""
        }

        let slice = Array(bytes[start..<(start + length)])
        return String(bytes: slice, encoding: .ascii)?
            .trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters)) ?? ""
    }

    private func updateScreen() {
        if self.apduRunner.callback != nil {
            self.apduRunner.callback?.complete(emoney: self.emoney)
        }
    }
}
