{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Network.Linode.Types where

import           Control.Applicative (optional)
import           Control.Exception   (IOException)
import           Control.Monad       (guard, mzero)
import           Data.Aeson
import           Data.Binary
import qualified Data.Map            as M
import           Data.Maybe          (fromJust, fromMaybe, isJust, mapMaybe)
import           Data.Text
import           GHC.Generics        (Generic)
import           Safe                (readMay)

type ApiKey = String

data LinodeCreationOptions = LinodeCreationOptions {
  datacenterChoice   :: String,
  planChoice         :: String,
  kernelSelect       :: [Kernel] -> Maybe Kernel,
  distributionSelect :: [Distribution] -> Maybe Distribution,
  paymentChoice      :: PaymentTerm,
  swapAmount         :: Int,  -- MB
  password           :: String,
  sshKey             :: Maybe String,
  diskLabel          :: String,
  config             :: Maybe ConfigId
}


newtype ConfigId = ConfigId {unConfigId :: Int}
  deriving (Eq, Show, Generic)

newtype DatacenterId = DatacenterId Int
  deriving (Eq, Ord, Show)

newtype DistributionId = DistributionId Int
  deriving (Eq, Ord, Show)

newtype DiskId = DiskId {unDisk :: Int}
  deriving (Eq, Show)

newtype LinodeId = LinodeId {unLinodeId :: Int}
  deriving (Eq, Ord, Show, Generic)

newtype JobId = JobId Int
  deriving (Eq, Show)

newtype KernelId = KernelId Int
  deriving (Eq, Ord, Show)

newtype PlanId = PlanId Int
  deriving (Eq, Ord, Show)



data DiskType = Ext3 | Ext4 | Swap | RawDisk
  deriving (Eq, Show)

data InstanceStatus = BeingCreated | NewInstance | Running | PoweredOff -- -1: Being Created, 0: Brand New, 1: Running, and 2: Powered Off.
  deriving (Eq, Show)

data PaymentTerm = OneMonth | OneYear | TwoYears
  deriving (Eq, Show)



-- TODO: "ACTIVE_SINCE":"2011-09-23 15:08:13.0",
data AccountInfo = AccountInfo {
  accountTransferPool     :: Int,
  accountTransferUsed     :: Int,
  accountTransferBillable :: Int,
  accountManaged          :: Bool,
  accountBalance          :: Int,
  accountBillingMethod    :: Text
} deriving (Eq, Show)

data Address = Address {
  ip       :: String,
  rdnsName :: String,
  isPublic :: Bool
} deriving (Eq, Show, Generic)

data Datacenter = Datacenter {
  datacenterId       :: DatacenterId,
  datacenterLocation :: Text,
  datacenterName     :: Text
} deriving (Eq, Show)

data Distribution = Distribution {
  distributionId      :: DistributionId,
  distributionName    :: Text,
  is64Bit             :: Bool,
  minImageSize        :: Int,
  requiresPvopsKernel :: Bool -- TODO explain
} deriving (Eq, Show)

{-
TODO missing fields
"WATCHDOG":1,
"LPM_DISPLAYGROUP":"",
"ALERT_BWQUOTA_ENABLED":1,
"ALERT_DISKIO_THRESHOLD":1000,
"BACKUPWINDOW":1,
"ALERT_BWOUT_ENABLED":1,
"ALERT_BWOUT_THRESHOLD":5,
"ALERT_CPU_ENABLED":1,
"ALERT_BWQUOTA_THRESHOLD":80,
"ALERT_BWIN_THRESHOLD":5,
"BACKUPWEEKLYDAY":0,
"ALERT_CPU_THRESHOLD":90,
"ALERT_DISKIO_ENABLED":1,
"ALERT_BWIN_ENABLED":1,
"CREATE_DT":"2015-09-22 11:33:06.0",
"DISTRIBUTIONVENDOR": "Debian",
"ISXEN":0,
"ISKVM":1
-}

{-|
Detailed info about a Linode instance. Memory and transfer are given in MB.
-}
data Instance = Instance {
  instanceId            :: LinodeId, -- "LINODEID":8098,
  instanceName          :: Text, -- LABEL
  instanceDatacenterId  :: DatacenterId, -- "DATACENTERID"
  instancePlanId        :: PlanId, -- "PLANID":1,
  instanceRAM           :: Int, -- "TOTALRAM":1024,
  instanceHD            :: Int, -- "TOTALHD":40960,
  instanceTransfer      :: Int, -- "TOTALXFER":2000,
  instanceBackupEnabled :: Bool, -- "BACKUPSENABLED":1,
  instanceStatus        :: InstanceStatus -- "STATUS" -- :2,
} deriving (Eq, Show)

data Kernel = Kernel {
  kernelId   :: KernelId,
  kernelName :: Text,
  isXen      :: Bool,
  isKVM      :: Bool,
  isPVOPS    :: Bool
} deriving (Eq, Show)

data Plan = Plan {
  planId         :: PlanId,
  planName       :: Text,
  ram            :: Int,
  disk           :: Int,
  xfer           :: Int,
  hourly         :: Double,
  availabilities :: M.Map DatacenterId Int
} deriving (Eq, Show)




data BootedInstance = BootedInstance {
  bootJobId :: JobId
} deriving (Eq, Show)

data CreatedConfig = CreatedConfig {
  createdConfigId :: ConfigId
} deriving (Eq, Show)

data CreatedLinode = CreatedLinode {
  createdLinodeId :: LinodeId
} deriving (Eq, Show)

data CreatedDisk = CreatedDisk {
  diskCreationDiskId :: DiskId,
  diskCreationJobId  :: JobId
} deriving (Eq, Show)

type DeletedLinode = CreatedLinode

data WaitingJob = WaitingJob {
  waitingJobId       :: JobId,
  waitingJobLinodeId :: LinodeId,
  waitingJobSuccess  :: Bool,
  waitingJobLabel    :: Text
} deriving (Eq, Show)

{-|
Basic info about a linode instance.
-}
data Linode = Linode {
  linodeId             :: LinodeId,
  linodeConfigId       :: ConfigId,
  linodeDatacenterName :: Text,
  linodePassword       :: String,
  linodeAddresses      :: [Address]
} deriving (Eq, Show, Generic)

instance Binary ConfigId

instance Binary LinodeId

instance Binary Address

instance Binary Linode

type Cluster = [Linode]


data Response a = Response {
  responseErrors  :: [LinodeError],
  responseContent :: Maybe a
} deriving (Eq, Show)


data LinodeError = BadRequest
                 | NoActionWasRequested
                 | TheRequestedClassDoesNotExist
                 | AuthenticationFailed
                 | ObjectNotFound
                 | ARequiredPropertyIsMissingForThisAction
                 | PropertyIsInvalid
                 | ADataValidationErrorHasOccurred
                 | MethodNotImplemented
                 | TooManyBatchedRequests
                 | RequestArrayIsntValidJSONOrWDDX
                 | BatchApproachingTimeout
                 | PermissionDenied
                 | APIRateLimitExceeded
                 | ChargingTheCreditCardFailed
                 | CreditCardIsExpired
                 | LimitOfLinodesAddedPerHourReached
                 | LinodeMustHaveNoDisksBeforeDelete
                 | DeserializationError Text
                 | NetworkError IOException
                 | UnknownError Int
                 | SelectionError String
  deriving (Eq, Show)


-----------------------------------------------
-- Json instances

instance FromJSON AccountInfo where
  parseJSON (Object v) = AccountInfo <$> v .: "TRANSFER_POOL" <*> v .: "TRANSFER_USED" <*> v .: "TRANSFER_BILLABLE" <*> v .: "MANAGED" <*> v .: "BALANCE" <*> v .: "BILLING_METHOD"
  parseJSON _ = mzero

instance FromJSON BootedInstance where
  parseJSON (Object v) = BootedInstance <$> (JobId <$> v .: "JobID")
  parseJSON _ = mzero

instance FromJSON CreatedLinode where
  parseJSON (Object v) = CreatedLinode <$> (LinodeId <$> v .: "LinodeID")
  parseJSON _ = mzero

instance FromJSON CreatedConfig where
  parseJSON (Object v) = CreatedConfig <$> (ConfigId <$> v .: "ConfigID")
  parseJSON _ = mzero

instance FromJSON CreatedDisk where
  parseJSON (Object v) = CreatedDisk <$> (DiskId <$> v .: "DiskID") <*> (JobId <$> v .: "JobID")
  parseJSON _ = mzero

instance FromJSON Datacenter where
  parseJSON (Object v) = Datacenter <$> (DatacenterId <$> v .: "DATACENTERID") <*> v .: "LOCATION" <*> v .: "ABBR"
  parseJSON _ = mzero

--  TODO: missing: "CREATE_DT":"2007-04-18 00:00:00.0",
instance FromJSON Distribution where
  parseJSON = withObject "distribution" $ \o -> do
    is64 <- o .: "IS64BIT"
    requires <- o .: "REQUIRESPVOPSKERNEL"
    guard (Prelude.all (`elem` [0,1]) [is64,requires])
    Distribution <$> fmap DistributionId (o .: "DISTRIBUTIONID") <*>  o .: "LABEL" <*> pure (isTrue is64) <*> o .: "MINIMAGESIZE" <*> pure (isTrue requires)
      where isTrue = (== (1::Int))

instance FromJSON Instance where
  parseJSON = withObject "instance" $ \o -> do
    s <- o .: "STATUS"
    let status = instanceStatusFromInt s
    guard (isJust status)
    backup <- o .: "BACKUPSENABLED"
    guard (Prelude.all (`elem` [0,1]) [backup])
    Instance <$> fmap LinodeId (o .: "LINODEID")
             <*>  o .: "LABEL"
             <*> fmap DatacenterId (o .: "DATACENTERID")
             <*> fmap PlanId (o .: "PLANID")
             <*> o .: "TOTALRAM"
             <*> o .: "TOTALHD"
             <*> o .: "TOTALXFER"
             <*> pure (isTrue backup)
             <*> pure (fromJust status)
      where isTrue = (== (1::Int))


instance FromJSON Address where
  parseJSON (Object o) = Address <$> o .: "IPADDRESS" <*>  o .: "RDNS_NAME" <*> (isTrue <$> (o .: "ISPUBLIC"))
    where isTrue = (== (1::Int))
  parseJSON _ = mzero


instance FromJSON Kernel where
  parseJSON = withObject "kernel" $ \o -> do
    xen <- o .: "ISXEN"
    kvm <- o .: "ISKVM"
    pvops <- o .: "ISPVOPS"
    guard (Prelude.all (`elem` [0,1]) [xen,kvm,pvops])
    Kernel <$> fmap KernelId (o .: "KERNELID") <*>  o .: "LABEL" <*> pure (isTrue xen) <*> pure (isTrue kvm) <*> pure (isTrue pvops)
      where isTrue = (== (1::Int))

instance FromJSON LinodeError where
  parseJSON = withObject "error response" $ \o -> do
    errorCode <- o .: "ERRORCODE"
    return $ linodeErrorFromCode errorCode

instance FromJSON Plan where
  parseJSON = withObject "plan" $ \o -> do
    d <- o .: "AVAIL"
    let toAvail = M.fromList . mapMaybe (\(k,v) -> may (DatacenterId <$> readMay k,v)) . M.toList
    Plan <$> (PlanId <$> o .: "PLANID") <*>  o .: "LABEL" <*> o .: "RAM" <*> o .: "DISK" <*> o .: "XFER" <*> o .: "HOURLY" <*> pure (toAvail d)
      where may (Nothing, _) = Nothing
            may (Just a, b) = Just (a,b)

instance FromJSON WaitingJob where
  parseJSON = withObject "person" $ \o -> do
    j <- JobId <$> o .: "JOBID"
    i <- LinodeId <$> o .: "LINODEID"
    l <- o.: "LABEL"
    success :: Maybe Int  <- optional (o .: "HOST_SUCCESS") -- 1 if ok, "" if not
    return $ WaitingJob j i (fromMaybe 0 success == 1) l


instance FromJSON a => FromJSON (Response a) where
  parseJSON = withObject "list response" $ \o -> do
    errs <- o .: "ERRORARRAY"
    contentList <- optional (o.: "DATA") -- when ERRORARRAY is not empty, this field is malformed ('{}') and we default it to []
    return $ Response errs contentList



linodeErrorFromCode :: Int -> LinodeError
linodeErrorFromCode 1 = BadRequest
linodeErrorFromCode 2 = NoActionWasRequested
linodeErrorFromCode 3 = TheRequestedClassDoesNotExist
linodeErrorFromCode 4 = AuthenticationFailed
linodeErrorFromCode 5 = ObjectNotFound
linodeErrorFromCode 6 = ARequiredPropertyIsMissingForThisAction
linodeErrorFromCode 7 = PropertyIsInvalid
linodeErrorFromCode 8 = ADataValidationErrorHasOccurred
linodeErrorFromCode 9 = MethodNotImplemented
linodeErrorFromCode 10 = TooManyBatchedRequests
linodeErrorFromCode 11 = RequestArrayIsntValidJSONOrWDDX
linodeErrorFromCode 12 = BatchApproachingTimeout
linodeErrorFromCode 13 = PermissionDenied
linodeErrorFromCode 14 = APIRateLimitExceeded
linodeErrorFromCode 30 = ChargingTheCreditCardFailed
linodeErrorFromCode 31 = CreditCardIsExpired
linodeErrorFromCode 40 = LimitOfLinodesAddedPerHourReached
linodeErrorFromCode 41 = LinodeMustHaveNoDisksBeforeDelete
linodeErrorFromCode x = UnknownError x

instanceStatusFromInt :: Int -> Maybe InstanceStatus
instanceStatusFromInt n = lookup n m
  where m = [(-1, BeingCreated),(0, NewInstance),(1, Running),(2, PoweredOff)]