namespace FundsSplitter.Core.Bot.Handlers

module PaymentHandler = 
    open System

    open FundsSplitter.Core
    open FundsSplitter.Core.Storage
    open FundsSplitter.Core.Strings
    open FundsSplitter.Core.Bot.Types
    open FundsSplitter.Core.Bot.Message
    open FundsSplitter.Core.Transactions.Types
    open FundsSplitter.Core.Transactions.CrudOperations
    open FundsSplitter.Core.Transactions.ProcessingLogic

    open Telegram.Bot
    open Telegram.Bot.Types

    let ChatNotFoundErrorEn = "There's no Funds Splitter group in this chat."
    let MessageDoesntContainAmountEn = "I couldn't find amount in your message."
    let AmountMustBePositiveEn = "Amount value must be possitive."
    let YouAreNotInTheGroupEn = "You aren't in the Funds Splitter group. To joint the group send me a /join command."

    let PayCommandAnswerEn amount description = 
        sprintf 
            "Amount: %M UAH.\n%s\nChoose members to split between:" 
            amount
            (if description |> String.IsNullOrEmpty then String.Empty else description |> sprintf "Description: `%s`." )

    let ChatNotFoundErrorRu = "–í —ç—Ç–æ–º —á–∞—Ç–µ –Ω–µ—Ç –≥—Ä—É–ø–ø—ã Funds Splitter."
    let MessageDoesntContainAmountRu = "–Ø –Ω–µ –º–æ–≥—É –Ω–∞–π—Ç–∏ —Å—É–º–º—É –≤ –≤–∞—à–µ–º —Å–æ–æ–±—â–µ–Ω–∏–∏."
    let AmountMustBePositiveRu = "–ó–Ω–∞—á–µ–Ω–∏–µ —Å—É–º–º—ã –¥–æ–ª–∂–Ω–æ –±—ã—Ç—å –ø–æ–ª–æ–∂–∏—Ç–µ–ª—å–Ω—ã–º."
    let YouAreNotInTheGroupRu = "–í—ã –Ω–µ –¥–æ–±–∞–≤–ª–µ–Ω–Ω—ã –≤ –≥—Ä—É–ø–ø—É Funds Splitter. –ß—Ç–æ–±—ã –ø—Ä–∏—Å–æ–µ–¥–µ–Ω–∏—Ç—å—Å—è –∫ –≥—Ä—É–ø–ø–µ –æ—Ç–ø—Ä–∞–≤—å—Ç–µ –º–Ω–µ –∫–æ–º–º–∞–Ω–¥—É /join."
    let PayCommandAnswerRu amount description = 
        sprintf 
            "–°—É–º–º–∞: %M –≥—Ä–Ω.\n%s\n–ú–µ–∂–¥—É –∫–µ–º —Ä–∞–∑–¥–µ–ª–∏—Ç—å?" 
            amount
            (if description |> String.IsNullOrEmpty then String.Empty else description |> sprintf "–û–ø–∏—Å–∞–Ω–∏–µ: `%s`." )

    let EverybodyButtonEn = "Everybody"
    let EverybodyButtonRu = "–ù–∞ –≤—Å–µ—Ö"

    let DeleteButtonEn = "üóë –£–¥–∞–ª–∏—Ç—å"
    let DeleteButtonRu = "üóë –£–¥–∞–ª–∏—Ç—å"

    let DeletedMessageEn = "Deleted..."
    let DeletedMessageRu = "–£–¥–∞–ª–µ–Ω–æ..."

    let SendTxEn = "Send payment"
    let SendTxRu = "–û—Ç–ø—Ä–∞–≤–∏—Ç—å —Ä–∞—Å—Ö–æ–¥"

    let ChatNotFoundError = function
        | En -> ChatNotFoundErrorEn
        | Ru -> ChatNotFoundErrorRu
    let MessageDoesntContainAmount = function
        | En -> MessageDoesntContainAmountEn
        | Ru -> MessageDoesntContainAmountRu
    let AmountMustBePositive = function
        | En -> AmountMustBePositiveEn
        | Ru -> AmountMustBePositiveRu
    let YouAreNotInTheGroup = function
        | En -> YouAreNotInTheGroupEn
        | Ru -> YouAreNotInTheGroupRu

    let PayCommandAnswer = function
        | En -> PayCommandAnswerEn
        | Ru -> PayCommandAnswerRu

    let EverybodyButton = function
        | En -> EverybodyButtonEn
        | Ru -> EverybodyButtonRu

    let DeleteButton = function
        | En -> DeleteButtonEn
        | Ru -> DeleteButtonRu

    let DeletedMessage = function
        | En -> DeletedMessageEn
        | Ru -> DeletedMessageRu

    let SendTx = function
        | En -> SendTxEn
        | Ru -> SendTxRu    

    type TxRaw = 
        {
            Chat: FundsSplitter.Core.Transactions.Types.Chat
            Description: string
            Amount: decimal
        }

    let parseText lang (msg: Telegram.Bot.Types.Message) chat = 
        let text = msg.Text |> replaceSpaces
        let words = text.Split(' ')
        let amount = words |> Array.tryFind (fun w -> Decimal.TryParse(w) |> fst)
        match amount with
        | None -> lang |> MessageDoesntContainAmount |> Error
        | Some a -> 
            let amount' = a.Replace(",", ".") |> Decimal.Parse 
            if amount' <= 0M then lang |> AmountMustBePositive |> Error
            else

            {
                Chat = chat
                Description = String.Empty // TODO: Add handling of description here
                Amount = amount'
            } |> Ok

    let createTx lang (msg: Message) tx = 
        let author' = tx.Chat.KnownUsers |> List.tryFind (fun u -> u.Id = msg.From.Id)
        if author' |> Option.isNone 
        then lang |> YouAreNotInTheGroup |> Error
        else
        let author = author'.Value
        
        let tx' = 
            {
                Id = Guid.NewGuid()

                User = author
                Message = {
                    Id = msg.MessageId
                    Text = msg.Text
                }
                Type = Payment
                Amount = tx.Amount
                SplittingSubset = []
            }
        (tx, tx') |> Ok

    let divideByRows rowSize list = 
        list
        |> List.fold (fun (acc: 'a list list) e -> 
                        if acc.[0].Length = rowSize then
                            [e] :: acc
                        else
                            (e :: acc.[0]) :: (acc |> List.skip 1)
                         ) [[]]

    let generateReplyMarkup lang users = 
        let userButtons = 
            users
            |> List.map (fun (u, isChecked) ->
                let icon = if isChecked then "‚úÖ" else "‚ùå"
                let btn = ReplyMarkups.InlineKeyboardButton()
                btn.Text <- sprintf "%s %s" icon (formatUser u) // ‚ùå ‚úÖ
                btn.CallbackData <- sprintf "toggle_splitting_subset#%s" (u.Id.ToString())
                btn)

        let everybodyButton = ReplyMarkups.InlineKeyboardButton()
        everybodyButton.Text <- EverybodyButton lang
        everybodyButton.CallbackData <- "toggle_splitting_subset#everybody" 

        let deleteBtn = ReplyMarkups.InlineKeyboardButton()
        deleteBtn.Text <- DeleteButton lang
        deleteBtn.CallbackData <- "delete_tx"

        let rows = 
            (divideByRows 2 userButtons) @ [[everybodyButton]] @ [[deleteBtn]]
            |> Seq.map (Seq.ofList)

        ReplyMarkups.InlineKeyboardMarkup(rows)

    let handlerFunction botContext (update: Update) = 
        fun () -> async {
            let msg = update.Message
            let client = botContext.BotClient
            let db = botContext.Storage.Database
            let chats = db.GetCollection(Collections.Chats)
            let cts = botContext.CancellationToken
            let lang = getLanguageCode update

            let tryFetchChat chatId = async {
                let! chat = tryFindChat chats cts chatId
                match chat with
                | Some c -> return Ok c
                | None -> return lang |> ChatNotFoundError |> Error
            }

            let addTx (tx, tx') = 
                tx'
                |> addTransaction tx.Chat
                |> upsertChat cts chats |> ignore
                (tx', tx)
                |> Ok

            let sendReplyMessage lang (res: Result<(Tx*TxRaw), string> ) = async {
                match res with
                | Error e -> return! sendAnswer client msg cts (Error e)
                | Ok (tx': Tx, tx: TxRaw) ->
                let txt = PayCommandAnswer lang tx'.Amount tx.Description

                let replyMarkup = 
                    tx.Chat.KnownUsers
                    |> List.map (fun u -> (u, false))
                    |> generateReplyMarkup lang

                
                let! _ =  client.SendTextMessageAsync(new ChatId(msg.Chat.Id), txt, Enums.ParseMode.Default, true, false, msg.MessageId, replyMarkup, cts)
                            |> Async.AwaitTask
                return Ok ()
            }

            return!
                msg.Chat.Id
                |> tryFetchChat
                |> AsyncResult.bind (parseText lang msg)
                |> AsyncResult.bind (createTx lang msg)
                |> AsyncResult.bind addTx
                |> Async.bind (sendReplyMessage lang)
        } |> Some

    let replyToggleUser botContext (update: Update) = 
        fun () -> async {
            let msg = update.CallbackQuery.Message
            let client = botContext.BotClient
            let db = botContext.Storage.Database
            let chats = db.GetCollection(Collections.Chats)
            let cts = botContext.CancellationToken
            let lang = getLanguageCode update

            let tryFetchChat chatId = async {
                let! chat = tryFindChat chats cts chatId
                return chat.Value
            }

            let toggleUser (chat: Transactions.Types.Chat) = 
                let tx = chat.Transactions |> List.find (fun tx -> tx.Message.Id = msg.ReplyToMessage.MessageId)
                if tx.User.Id <> update.CallbackQuery.From.Id then
                    Error "Access"
                else
                let parsedPayload = update.CallbackQuery.Data.Split('#').[1]
                if parsedPayload = "everybody" 
                then // Everybody case
                    if tx.SplittingSubset.Length = chat.KnownUsers.Length then
                        Ok ({ tx with SplittingSubset = [] }, chat)
                    else
                    Ok ({ tx with SplittingSubset = chat.KnownUsers }, chat)

                else // Particular member case
                let userId = Int32.Parse(update.CallbackQuery.Data.Split('#').[1])
                let user = chat.KnownUsers |> List.find (fun u -> u.Id = userId)
                if tx.SplittingSubset |> List.exists (fun u -> u.Id = userId) then
                    Ok ({ tx with SplittingSubset = tx.SplittingSubset |> List.filter (fun u -> u.Id <> user.Id)}, chat)
                else
                    Ok ({ tx with SplittingSubset = user :: tx.SplittingSubset }, chat)

            let saveTx (tx, chat) =  
                tx
                |> editTransaction chat
                |> upsertChat cts chats |> ignore
                (tx, chat)

            let sendReplyAnswer (tx, chat) = async {
                let replyMarkup = 
                    chat.KnownUsers 
                    |> List.map (fun u -> (u, tx.SplittingSubset |> List.exists (fun u' -> u'.Id = u.Id)))
                    |> generateReplyMarkup lang

                let! _ = client.EditMessageReplyMarkupAsync(ChatId msg.Chat.Id, msg.MessageId, replyMarkup, cts)
                            |> Async.AwaitTask

                return Ok ()
            }

            let! res = 
                msg.Chat.Id
                |> tryFetchChat
                |> Async.map toggleUser
                |> AsyncResult.map saveTx
                |> AsyncResult.bindAsync sendReplyAnswer 
                |> Async.map (Result.mapError ignore)

            return res
        } |> Some

    let replyDeleteTx botContext (update: Update) = 
        fun () -> async {
            let msg = update.CallbackQuery.Message
            let client = botContext.BotClient
            let db = botContext.Storage.Database
            let chats = db.GetCollection(Collections.Chats)
            let cts = botContext.CancellationToken
            let lang = getLanguageCode update

            let tryFetchChat chatId = async {
                let! chat = tryFindChat chats cts chatId
                return chat.Value
            }

            let deleteTx chat =  
                let tx = chat.Transactions |> List.find (fun tx -> tx.Message.Id = msg.ReplyToMessage.MessageId)

                if tx.User.Id <> update.CallbackQuery.From.Id then
                    Error "Access"
                else

                tx
                |> deleteTransaction chat
                |> upsertChat cts chats |> ignore
                Ok ()

            let sendReplyAnswer () = async {
                let replyMarkup = ReplyMarkups.InlineKeyboardMarkup(Seq.empty : System.Collections.Generic.IEnumerable<ReplyMarkups.InlineKeyboardButton>)

                let! _ = client.EditMessageTextAsync(ChatId(msg.Chat.Id), msg.MessageId, (DeletedMessage lang), Telegram.Bot.Types.Enums.ParseMode.Default, true, replyMarkup, cts)
                            |> Async.AwaitTask

                return Ok ()
            }

            let! res = 
                msg.Chat.Id
                |> tryFetchChat
                |> Async.map deleteTx
                |> AsyncResult.bindAsync sendReplyAnswer
                |> Async.map (Result.mapError ignore)

            return res
        } |> Some

    let replyInlineQuery botContext (update: Update) = 
        fun () -> async {
            let client = botContext.BotClient
            let lang = getLanguageCode update

            let commandText = sprintf "/pay %s" update.InlineQuery.Query
            let results = [InlineQueryResults.InlineQueryResultArticle("1", SendTx lang,  InlineQueryResults.InputTextMessageContent(commandText)) :> InlineQueryResults.InlineQueryResultBase]
                            |> Seq.ofList
            let! _ = client.AnswerInlineQueryAsync(update.InlineQuery.Id, results)
                        |> Async.AwaitTask
        

            return Ok ()
        } |> Some
