//
//  NoteZapButton.swift
//  damus
//
//  Created by William Casarin on 2023-01-17.
//

import SwiftUI
import Sentry

enum ZappingEventType {
    case failed(ZappingError)
    case got_zap_invoice(String)
    case sent_from_nwc
}

enum ZappingError {
    case fetching_invoice
    case bad_lnurl
    case canceled
    case send_failed
    
    func humanReadableMessage() -> String {
        switch self {
            case .fetching_invoice:
                return NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
            case .bad_lnurl:
                return NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
            case .canceled:
                return NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.")
            case .send_failed:
                return NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
        }
    }
}

struct ZappingEvent {
    let is_custom: Bool
    let type: ZappingEventType
    let target: ZapTarget
}

struct NoteZapButton: View {
    let damus_state: DamusState
    let target: ZapTarget
    let lnurl: String
    
    @ObservedObject var zaps: ZapsDataModel
    
    var our_zap: Zapping? {
        zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey })
    }
    
    var zap_img: String {
        switch our_zap {
        case .none:
            return "zap"
        case .zap:
            return "zap.fill"
        case .pending:
            return "zap.fill"
        }
    }
    
    var zap_color: Color {
        if our_zap == nil {
            return Color.gray
        }
        
        // always orange !
        return Color.orange
    }
    
    func tap() async {
        guard let our_zap else {
            Task { await send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) }
            return
        }
        
        // we've tapped and we have a zap already... cancel if we can
        switch our_zap {
        case .zap:
            // can't undo a zap we've already sent
            // if we want to send more zaps we will need to long-press
            print("cancel_zap: we already have a real zap, can't cancel")
            break
        case .pending(let pzap):
            guard let res = await cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
                
                UIImpactFeedbackGenerator(style: .soft).impactOccurred()
                return
            }
            
            UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
            
            switch res {
            case .send_err(let cancel_err):
                switch cancel_err {
                case .nothing_to_cancel:
                    print("cancel_zap: got nothing_to_cancel in pending")
                    break
                case .not_delayed:
                    print("cancel_zap: got not_delayed in pending")
                    break
                case .too_late:
                    print("cancel_zap: got too_late in pending")
                    break
                }
            case .already_confirmed:
                print("cancel_zap: got already_confirmed in pending")
                break
            case .not_nwc:
                print("cancel_zap: got not_nwc in pending")
                break
            }
        }
                
            
    }
    
    var body: some View {
        HStack(spacing: 4) {
            if !damus_state.settings.nozaps || zaps.zap_total > 0 {
                Button(action: {
                }, label: {
                    Image(zap_img)
                        .resizable()
                        .foregroundColor(zap_color)
                        .font(.footnote.weight(.medium))
                        .aspectRatio(contentMode: .fit)
                        .frame(width:20, height: 20)
                })
            }

            if zaps.zap_total > 0 {
                Text(verbatim: format_msats_abbrev(zaps.zap_total))
                    .font(.footnote)
                    .foregroundColor(zap_color)
            }
        }
        .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
        .simultaneousGesture(LongPressGesture().onEnded {_  in
            guard !damus_state.settings.nozaps else { return }
            
            present_sheet(.zap(target: target, lnurl: lnurl))
        })
        .highPriorityGesture(TapGesture().onEnded {
            guard !damus_state.settings.nozaps else { return }
            
            Task { await tap() }
        })
    }
}


struct ZapButton_Previews: PreviewProvider {
    static var previews: some View {
        let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
        let zaps = ZapsDataModel([.pending(pending_zap)])
        
        NoteZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps)
    }
}



func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
    if let url = settings.nostr_wallet_connect,
       let nwc = WalletConnectURL(str: url)
    {
        return .nwc(NWCPendingZapState(state: .fetching_invoice, url: nwc))
    }
    
    return .external(ExtPendingZapState(state: .fetching_invoice))
}

func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) async {
    guard let keypair = damus_state.keypair.to_full() else {
        return
    }
    
    // Only take the first 10 because reasons
    let relays = Array(await damus_state.nostrNetwork.ourRelayDescriptors.prefix(10))
    let content = comment ?? ""
    
    guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
        // this should never happen
        DamusSentry.captureSentryMessage("Failed to create zap request event") { scope in
            scope.setContext(value: [
                "operation": "make_zap_request_event",
                "zap_type": String(describing: zap_type),
                "relay_count": relays.count,
                "has_comment": comment != nil
            ], key: "zaps")
        }
        return
    }
    
    let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000
    let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings)
    let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: mzapreq, type: zap_type, state: pending_zap_state)
    let zapreq = mzapreq.potentially_anon_outer_request.ev
    let reqid = ZapRequestId(from_makezap: mzapreq)
    
    UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
    damus_state.add_zap(zap: .pending(pending_zap))
    
    Task { @MainActor in
        guard let payreq = await damus_state.lnurls.lookup_or_fetch(pubkey: target.pubkey, lnurl: lnurl) else {
            // TODO: show error
            remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
            let typ = ZappingEventType.failed(.bad_lnurl)
            let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
            notify(.zapping(ev))
            DamusSentry.captureSentryMessage("Failed to fetch lnurl payreq for zap") { scope in
                scope.setContext(value: [
                    "operation": "zap_fetch_lnurl"
                ], key: "zaps")
            }
            return
        }

        guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
            remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
            let typ = ZappingEventType.failed(.fetching_invoice)
            let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
            notify(.zapping(ev))
            DamusSentry.captureSentryMessage("Failed to fetch zap invoice") { scope in
                scope.setContext(value: [
                    "operation": "zap_fetch_invoice",
                    "zap_type": String(describing: zap_type)
                ], key: "zaps")
            }
            return
        }

        switch pending_zap_state {
        case .nwc(let nwc_state):
            // don't both continuing, user has canceled
            if case .cancel_fetching_invoice = nwc_state.state {
                remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
                let typ = ZappingEventType.failed(.canceled)
                let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
                notify(.zapping(ev))
                return
            }

            var flusher: OnFlush? = nil

            // donations are only enabled on one-tap zaps and off appstore
            if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
                flusher = .once({ pe in
                    // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
                    Task { @MainActor in
                        await damus_state.nostrNetwork.send_donation_zap(nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
                    }
                })
            }

            // we don't have a delay on one-tap nozaps (since this will be from customize zap view)
            let delay = damus_state.settings.nozaps ? nil : 5.0

            let nwc_req = await damus_state.nostrNetwork.nwcPay(url: nwc_state.url, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher)

            guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
                print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")

                let typ = ZappingEventType.failed(.send_failed)
                let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
                notify(.zapping(ev))
                DamusSentry.captureSentryMessage("Failed to send NWC zap request") { scope in
                    scope.setContext(value: [
                        "operation": "nwc_pay_request",
                    ], key: "zaps")
                }
                return
            }

            print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")

            if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
                // we don't need to trigger a ZapsDataModel update here
            }

            let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
            notify(.zapping(ev))

        case .external(let pending_ext):
            pending_ext.state = .done
            let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
            notify(.zapping(ev))
        }
    }
    
    return
}

enum CancelZapErr {
    case send_err(CancelSendErr)
    case already_confirmed
    case not_nwc
}

func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) async -> CancelZapErr? {
    guard case .nwc(let nwc_state) = zap.state else {
        return .not_nwc
    }
    
    switch nwc_state.state {
    case .fetching_invoice:
        if nwc_state.update_state(state: .cancel_fetching_invoice) {
            // we don't need to update the ZapsDataModel here
        }
        // let the code that retrieves the invoice remove the zap, because
        // it still needs access to this pending zap to know to cancel
        
    case .cancel_fetching_invoice:
        // already cancelling?
        print("cancel_zap: already cancelling")
        return nil
        
    case .confirmed:
        return .already_confirmed
        
    case .postbox_pending(let nwc_req):
        if let err = await box.cancel_send(evid: nwc_req.id) {
            return .send_err(err)
        }
        let reqid = ZapRequestId(from_pending: zap)
        remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
        
    case .failed:
        let reqid = ZapRequestId(from_pending: zap)
        remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
    }
    
    return nil
}
