{-# LANGUAGE OverloadedStrings #-}

module Zenith.Utils where

import Control.Exception (SomeException, try)
import Control.Monad (when)
import Data.Aeson
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import Data.Aeson.Types (parseMaybe)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Char (isAlphaNum, isSpace)
import Data.Functor (void)
import Data.Maybe
import Data.Ord (clamp)
import Data.Scientific (Scientific(..), Scientific, scientific, toRealFloat)
import qualified Data.Text as T
import qualified Data.Text.Encoding as E

--import qualified Data.Text.Encoding as TE
import Network.HTTP.Simple
import Network.URI (escapeURIString, isUnreserved)
import System.Directory
import System.Process (createProcess_, shell)
import Text.Printf (printf)
import Text.Read (readMaybe)
import Text.Regex.Posix
import ZcashHaskell.Orchard
  ( encodeUnifiedAddress
  , isValidUnifiedAddress
  , parseAddress
  )
import ZcashHaskell.Sapling (decodeSaplingAddress, isValidShieldedAddress)
import ZcashHaskell.Transparent
  ( decodeExchangeAddress
  , decodeTransparentAddress
  )
import ZcashHaskell.Types
  ( ExchangeAddress(..)
  , ExchangeAddress(..)
  , SaplingAddress(..)
  , TransparentAddress(..)
  , UnifiedAddress(..)
  , ValidAddress(..)
  , ZcashNet(..)
  , Phrase(..)
  )
import ZcashHaskell.Utils (makeZebraCall)
import Zenith.Types
  ( AddressGroup(..)
  , PrivacyPolicy(..)
  , UnifiedAddressDB(..)
  , ZcashAddress(..)
  , ZcashPaymentURI(..)
  , ZcashPool(..)
  )

-- | Helper function to convert numbers into JSON
jsonNumber :: Int -> Value
jsonNumber i = Number $ scientific (fromIntegral i) 0

-- | Helper function to display small amounts of ZEC
displayZec :: Integer -> String
displayZec s
  | abs s < 100 = show s ++ " zats"
  | abs s < 100000 = show (fromIntegral s / 100) ++ " μZEC"
  | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mZEC"
  | otherwise = show (fromIntegral s / 100000000) ++ " ZEC "

-- | Helper function to display small amounts of TAZ
displayTaz :: Integer -> String
displayTaz s
  | abs s < 100 = show s ++ " tazs"
  | abs s < 100000 = show (fromIntegral s / 100) ++ " μTAZ"
  | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mTAZ"
  | otherwise = show (fromIntegral s / 100000000) ++ " TAZ"

displayAmount :: ZcashNet -> Integer -> T.Text
displayAmount n a =
  if n == MainNet
    then T.pack $ displayZec a
    else T.pack $ displayTaz a

-- | Helper function to display abbreviated Unified Address
showAddress :: UnifiedAddressDB -> T.Text
showAddress u = T.take 20 t <> "..."
  where
    t = getUA u

-- | Helper function to extract addresses from AddressGroups
getAddresses :: AddressGroup -> [ZcashAddress]
getAddresses ag = agtransparent ag <> agsapling ag <> agunified ag

-- | Helper function to validate potential Zcash addresses
validateAddress :: T.Text -> Maybe ZcashPool
validateAddress txt --(tReg || sReg && isJust chk) || (uReg && isJust chk)
  | tReg = Just TransparentPool
  | sReg && chkS = Just SaplingPool
  | uReg && chk = Just OrchardPool
  | otherwise = Nothing
  where
    transparentRegex = "^t1[a-zA-Z0-9]{33}$" :: String
    shieldedRegex = "^zs[a-zA-Z0-9]{76}$" :: String
    unifiedRegex = "^u[a-zA-Z0-9]" :: String
    tReg = T.unpack txt =~ transparentRegex :: Bool
    sReg = T.unpack txt =~ shieldedRegex :: Bool
    uReg = T.unpack txt =~ unifiedRegex :: Bool
    chk = isJust $ isValidUnifiedAddress $ E.encodeUtf8 txt
    chkS = isValidShieldedAddress $ E.encodeUtf8 txt

-- | Return True if Address is valid
validateAddressBool :: T.Text -> Bool
validateAddressBool a = do
  case (validateAddress a) of
    Nothing -> False
    _ -> True

-- | Copy an address to the clipboard
copyAddress :: ZcashAddress -> IO ()
copyAddress a =
  void $
  createProcess_ "toClipboard" $
  shell $ "echo " ++ T.unpack (addy a) ++ " | xclip -r -selection clipboard"

-- | Get current user and build zenith path
getZenithPath :: IO String
getZenithPath = do
  homeDirectory <- getHomeDirectory
  return (homeDirectory ++ "/Zenith/")

-- | Bound a value to the 0..1 range, used for progress reporting on UIs
validBarValue :: Float -> Float
validBarValue = clamp (0, 1)

isRecipientValid :: T.Text -> Bool
isRecipientValid a = do
  case isValidUnifiedAddress (E.encodeUtf8 a) of
    Just _a1 -> True
    Nothing ->
      isValidShieldedAddress (E.encodeUtf8 a) ||
      (case decodeTransparentAddress (E.encodeUtf8 a) of
         Just _a3 -> True
         Nothing ->
           case decodeExchangeAddress (E.encodeUtf8 a) of
             Just _a4 -> True
             Nothing -> False)

isUnifiedAddressValid :: T.Text -> Bool
isUnifiedAddressValid ua =
  case isValidUnifiedAddress (E.encodeUtf8 ua) of
    Just _a1 -> True
    Nothing -> False

isSaplingAddressValid :: T.Text -> Bool
isSaplingAddressValid sa = isValidShieldedAddress (E.encodeUtf8 sa)

isTransparentAddressValid :: T.Text -> Bool
isTransparentAddressValid ta =
  case decodeTransparentAddress (E.encodeUtf8 ta) of
    Just _a3 -> True
    Nothing -> False

isExchangeAddressValid :: T.Text -> Bool
isExchangeAddressValid xa =
  case decodeExchangeAddress (E.encodeUtf8 xa) of
    Just _a4 -> True
    Nothing -> False

isRecipientValidGUI :: PrivacyPolicy -> T.Text -> Bool
isRecipientValidGUI p a = do
  let adr = parseAddress (E.encodeUtf8 a)
  case p of
    Full ->
      case adr of
        Just a ->
          case a of
            Unified ua -> True
            Sapling sa -> True
            _ -> False
        Nothing -> False
    Medium ->
      case adr of
        Just a ->
          case a of
            Unified ua -> True
            Sapling sa -> True
            _ -> False
        Nothing -> False
    Low ->
      case adr of
        Just a ->
          case a of
            Unified ua -> True
            Sapling sa -> True
            Transparent ta -> True
            _ -> False
        Nothing -> False
    None ->
      case adr of
        Just a ->
          case a of
            Transparent ta -> True
            Exchange ea -> True
            _ -> False
        Nothing -> False

isZecAddressValid :: T.Text -> Bool
isZecAddressValid a = do
  case isValidUnifiedAddress (E.encodeUtf8 a) of
    Just _a1 -> True
    Nothing ->
      isValidShieldedAddress (E.encodeUtf8 a) ||
      (case decodeTransparentAddress (E.encodeUtf8 a) of
         Just _a3 -> True
         Nothing ->
           case decodeExchangeAddress (E.encodeUtf8 a) of
             Just _a4 -> True
             Nothing -> False)

parseAddressUA :: T.Text -> ZcashNet -> Maybe UnifiedAddress
parseAddressUA a znet =
  case isValidUnifiedAddress (E.encodeUtf8 a) of
    Just a1 -> Just a1
    Nothing ->
      case decodeSaplingAddress (E.encodeUtf8 a) of
        Just a2 ->
          Just $ UnifiedAddress znet Nothing (Just $ sa_receiver a2) Nothing
        Nothing ->
          case decodeTransparentAddress (E.encodeUtf8 a) of
            Just a3 ->
              Just $ UnifiedAddress znet Nothing Nothing (Just $ ta_receiver a3)
            Nothing -> Nothing

isValidContent :: String -> Bool
isValidContent [] = False --  an empty string is invalid   
isValidContent (x:xs)
  | not (isAlphaNum x) = False --  string must start with an alphanumeric character
  | otherwise = allValidChars xs --  process the rest of the string 
  where
    allValidChars :: String -> Bool
    allValidChars [] = True -- if we got here, string is valid
    allValidChars (y:ys)
      | isAlphaNum y || isSpace y = allValidChars ys -- char is valid, continue
      | otherwise = False -- found an invalid character, return false  

isValidString :: T.Text -> Bool
isValidString c = do
  let a = T.unpack c
  isValidContent a

padWithZero :: Int -> String -> String
padWithZero n s
  | length s >= n = s
  | otherwise = padWithZero n ("0" ++ s)

isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty _ = False

getChainTip :: T.Text -> Int -> IO Int
getChainTip zHost zPort = do
  r <- makeZebraCall zHost zPort "getblockcount" []
  case r of
    Left e1 -> pure 0
    Right i -> pure i

-- Function to fetch Zcash price from CoinGecko
getZcashPrice :: T.Text -> IO (Maybe Double)
getZcashPrice currency = do
  let url =
        "https://api.coingecko.com/api/v3/simple/price?ids=zcash&vs_currencies=" <>
        T.unpack currency
  response <- httpJSONEither (parseRequest_ url)
  case getResponseBody response of
    Right (Object obj)
        -- Extract "zcash" object
     -> do
      case KM.lookup "zcash" obj of
        Just (Object zcashObj)
            -- Extract the currency price
         ->
          case KM.lookup (K.fromText (T.toLower currency)) zcashObj of
            Just (Number price) -> return (Just (toRealFloat price))
            _ -> return Nothing
        _ -> return Nothing
    _ -> return Nothing

-- Parse memo result to convert it to a ByteString
processEither :: Either String BC.ByteString -> BC.ByteString
processEither (Right bs) = bs
processEither (Left e) = BC.pack e -- Returns the error message

-- Parse the query string into key-value pairs 
parseQuery :: String -> [(String, String)]
parseQuery query = map (breakOn '=') (splitOn '&' query)
  where
    splitOn :: Char -> String -> [String]
    splitOn _ [] = [""]
    splitOn delim (c:cs)
      | c == delim = "" : rest
      | otherwise = (c : head rest) : tail rest
      where
        rest = splitOn delim cs
    breakOn :: Char -> String -> (String, String)
    breakOn delim str = (key, drop 1 value)
      where
        (key, value) = span (/= delim) str

-- Parse a ZIP-321 encoded string into a ZcashPayment structure
parseZcashPayment :: String -> Either String ZcashPaymentURI
parseZcashPayment input
  | not (T.isPrefixOf "zcash:" (T.pack input)) =
    Left "Invalid scheme: must start with 'zcash:'"
  | otherwise =
    let (addrPart, queryPart) = break (== '?') (drop 6 input)
        queryParams = parseQuery (drop 1 queryPart)
     in Right
          ZcashPaymentURI
            { uriAddress = addrPart
            , uriAmount = lookup "amount" queryParams >>= readMaybe
            , uriMemo =
                case lookup "memo" queryParams of
                  Just m ->
                    T.pack
                      (BC.unpack
                         (processEither $ decodeBase64Unpadded (BC.pack m)))
                  _ -> ""
            , uriLabel = lookup "label" queryParams
            , uriMessage = lookup "message" queryParams
            }

-- Function to pad a base64 string if it's not a multiple of 4
padBase64 :: BC.ByteString -> BC.ByteString
padBase64 bs = bs <> BC.replicate paddingLength '='
  where
    paddingLength = (4 - BC.length bs `mod` 4) `mod` 4

-- Function to decode a base64 un-padded string
decodeBase64Unpadded :: BC.ByteString -> Either String BC.ByteString
decodeBase64Unpadded = B64.decode . padBase64

-- Function to encode memo as un-padded Base64
encodeBase64Memo :: String -> String
encodeBase64Memo = BC.unpack . BC.takeWhile (/= '=') . B64.encode . BC.pack

-- Function to drop trailing zeros 
dropTrailingZeros :: String -> String
dropTrailingZeros str =
  let withoutZeros = reverse (dropWhile (== '0') (reverse str))
   in if last withoutZeros == '.'
        then withoutZeros ++ "0" -- Ensure at least one decimal place
        else withoutZeros

-- Function to create a ZIP-321 URI
createZip321 :: String -> Maybe Double -> Maybe String -> String
createZip321 address mAmount mMemo =
  "zcash:" ++
  address ++
  maybe
    ""
    (\amount -> "?amount=" ++ dropTrailingZeros (printf "%.8f" amount))
    mAmount ++
  maybe
    ""
    (\memo -> "&memo=" ++ escapeURIString isUnreserved (encodeBase64Memo memo))
    mMemo

getTransparentFromUA :: UnifiedAddress -> Maybe TransparentAddress
getTransparentFromUA ua = TransparentAddress (ua_net ua) <$> t_rec ua

-- Function to check if Text is non-empty after trimming leading spaces
isNotEmptyAfterTrim :: T.Text -> Bool
isNotEmptyAfterTrim txt = not (T.null (T.stripStart txt))

-- Function to convert a Scientific number to Int 
scientificToInt :: Scientific -> Int
scientificToInt sc = fromIntegral $ round $ toRealFloat sc

-- Convert a ByteString to Phrase
toPhrase :: BS.ByteString -> Phrase
toPhrase = Phrase

