{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE EmptyDataDecls    #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

-- |Contains Haskell representation of most data types used for interacting with
-- DigitalOcean's API v2
--
-- See https://developers.digitalocean.com/documentation/v2/
module DigitalOcean where

import           Data.Aeson        as A
import           Data.Aeson.Types  as A
import           Data.Default
import qualified Data.HashMap.Lazy as H
import           Data.IP
import           Data.List         (elemIndex)
import           Data.Monoid       ((<>))
import           Data.Text         (unpack)
import           Data.Time         (UTCTime)
import           GHC.Generics


type AuthToken = String

type Slug = String

type URI = String

data ToolConfiguration = Tool { slackUri  :: Maybe URI
                              , authToken :: Maybe AuthToken
                              , quiet     :: Bool
                              } deriving (Show,Read)

instance Default ToolConfiguration where
  def = Tool Nothing Nothing False

-- | A type for describing @Region@
-- A region can be assigned an empty object when it is undefined, or be referenced simply
-- by its @slug@
-- https://developers.digitalocean.com/documentation/v2/#regions
data Region = Region { regionName      :: String
                     , regionSlug      :: Slug
                     , regionSizes     :: [ SizeSlug ]
                     , regionAvailable :: Bool
                     }
            | RegionSlug Slug
            | NoRegion

instance ToJSON Region where
  toJSON (RegionSlug s) = toJSON s
  toJSON  NoRegion      = object []
  toJSON Region{..}     = object [ "name" .= regionName
                                 , "slug" .= regionSlug
                                 , "sizes" .= regionSizes
                                 , "available" .= regionAvailable
                                 ]

instance Show Region where
  show (RegionSlug s) = s
  show  NoRegion      = "NoRegion"
  show Region{..}     = "Region { regionName = " <> show regionName <>
                        ", regionSlug = " <> show regionSlug <>
                        ", regionSizes = " <> show regionSizes <>
                        ", regionAvailable = " <> show regionAvailable <>
                        "}"


instance FromJSON Region where
  parseJSON (String s) = return $ RegionSlug (unpack s)
  parseJSON (Object o) = if H.null o
                         then return NoRegion
                         else Region
                              <$> o .: "name"
                              <*> o .: "slug"
                              <*> o .: "sizes"
                              <*> o .: "available"
  parseJSON e          = failParse e


-- | String representation of size slugs
-- This maps to corresponding expected JSON string value.
sizeSlugs :: [String]
sizeSlugs = [ "512mb", "1gb",  "2gb",  "4gb",  "8gb",  "16gb", "32gb", "48gb", "64gb", "96gb"  ]

-- | Enumeration of all possible size slugs
data SizeSlug = M512 | G1 | G2 | G4 | G8 | G16 | G32 | G48 | G64 | G96
              deriving (Enum,Ord,Eq)

instance Show SizeSlug where
  show sz = sizeSlugs !! fromEnum sz

instance Read SizeSlug where
  readsPrec _ sz = case elemIndex sz sizeSlugs of
                    Just i  -> return (toEnum i, "")
                    Nothing -> fail $ "cannot parse " <> sz

instance ToJSON SizeSlug where
  toJSON sz = toJSON $ sizeSlugs !! fromEnum sz

instance FromJSON SizeSlug where
  parseJSON (String s) = case elemIndex (unpack s) sizeSlugs of
                          Just i -> return $ toEnum i
                          Nothing -> fail $ "cannot parse " <> unpack s
  parseJSON e          = failParse e


type ImageSlug = String
type KeyId = Int

defaultImage :: ImageSlug
defaultImage = "ubuntu-14-04-x64"

data BoxConfiguration = BoxConfiguration { configName      :: String
                                         , boxRegion       :: Region
                                         , size            :: SizeSlug
                                         , configImageSlug :: ImageSlug
                                         , keys            :: [KeyId]
                                         } deriving (Show)

instance ToJSON BoxConfiguration where
  toJSON BoxConfiguration{..} = object [ "name"     .=  configName
                                       , "region"   .=  boxRegion
                                       , "size"     .=  size
                                       , "image"    .=  configImageSlug
                                       , "ssh_keys" .=  keys
                                       , "backups"  .=  False
                                       , "ipv6"     .=  False
                                       , "private_networking" .=  False
                                       ]

type Id = Integer

data Mega
data Giga

-- | A type for various sizes
-- Type parameter is used to define number's magnitude
newtype Bytes a = Bytes { bytesSize :: Int } deriving Show

jsonBytes :: Int -> Parser (Bytes a)
jsonBytes = return . Bytes

instance FromJSON (Bytes Mega) where
  parseJSON (Number n) = jsonBytes (truncate n)
  parseJSON e          = failParse e

instance FromJSON (Bytes Giga) where
  parseJSON (Number n) = jsonBytes (truncate n)
  parseJSON e          = failParse e

newtype Date = Date { theDate :: UTCTime } deriving Show

instance FromJSON Date where
  parseJSON d@(String _) = Date <$> parseJSON d
  parseJSON e            = failParse e

data Status = New
            | Active
            | Off
            | Archive
            deriving (Eq,Show)

instance FromJSON Status where
  parseJSON (String s) = case s of
                          "new" -> return New
                          "active" -> return Active
                          "off" -> return Off
                          "archive" -> return Archive
                          _        -> fail $ "cannot parse " <> unpack s
  parseJSON e          = failParse e

data NetType = Public | Private deriving (Show, Eq)

-- | Type of a single Network definition
--
-- This type is parameterized with a phantom type which lifts the network address type at
-- the type level (could use DataKinds extension...). This allows distinguishing types of
-- of networks while using same parsing.
data Network a = NetworkV4 { ip_address :: IP
                           , netmask    :: IP
                           , gateway    :: IP
                           , netType    :: NetType
                           }
               | NetworkV6 { ip_address :: IP
                           , netmask_v6 :: Int
                           , gateway    :: IP
                           , netType    :: NetType
                           } deriving Show

instance FromJSON IP where
  parseJSON (String s) = return $ read $ unpack s
  parseJSON e          = fail $ "cannot parse IP " <> show e

instance FromJSON NetType where
  parseJSON (String s) = case s of
                          "public" -> return Public
                          "private" -> return Private
                          e         -> failParse e
  parseJSON e          = failParse e

data V4
data V6

jsonNetwork :: (FromJSON a3, FromJSON a2, FromJSON a1, FromJSON a) => (a3 -> a2 -> a1 -> a -> b) -> Object -> Parser b
jsonNetwork f n = f
                  <$> (n .: "ip_address")
                  <*> (n .: "netmask")
                  <*> (n .: "gateway")
                  <*> (n .: "type")

instance FromJSON (Network V4) where
  parseJSON (Object n) = jsonNetwork NetworkV4 n
  parseJSON e          = failParse e

instance FromJSON (Network V6) where
  parseJSON (Object n) = jsonNetwork NetworkV6 n
  parseJSON e          = fail $ "cannot parse network v6 " <> show e


-- | Type of Networks configured for a @Droplet@
--
-- A network is either a list of IPv4 and IPv6 NICs definitions, or no network. We need this
-- because a droplet can contain an ''empty'' @networks@  JSON Object entry, instead of @null@.
data Networks = Networks { v4 :: [ Network V4 ]
                         , v6 :: [ Network V6 ]
                         }
                | NoNetworks
                deriving (Generic, Show)

instance FromJSON Networks where
  parseJSON (Object n) = if H.null n
                         then return NoNetworks
                         else Networks
                              <$> (n .: "v4")
                              <*> (n .: "v6")
  parseJSON e          = fail $ "cannot parse network v6 " <> show e

-- | (Partial) Type of Droplets
--
-- https://developers.digitalocean.com/documentation/v2/#droplets
data Droplet = Droplet { id           :: Id
                       , name         :: String
                       , memory       :: Bytes Mega
                       , vcpus        :: Int
                       , disk         :: Bytes Giga
                       , locked       :: Bool
                       , created_at   :: Date
                       , status       :: Status
                       , backup_ids   :: [ Id ]
                       , snapshot_ids :: [ Id ]
                       , region       :: Region
                       , size_slug    :: SizeSlug
                       , networks     :: Networks
                       } deriving (Show)

instance FromJSON Droplet where
  parseJSON (Object o) = Droplet
                         <$> o .: "id"
                         <*> o .: "name"
                         <*> o .: "memory"
                         <*> o .: "vcpus"
                         <*> o .: "disk"
                         <*> o .: "locked"
                         <*> o .: "created_at"
                         <*> o .: "status"
                         <*> o .: "backup_ids"
                         <*> o .: "snapshot_ids"
                         <*> o .: "region"
                         <*> o .: "size_slug"
                         <*> o .: "networks"
  parseJSON e          = fail $ "cannot parse network v6 " <> show e


data ImageType = Snapshot
               | Temporary
               | Backup
                 deriving Show

instance FromJSON ImageType where
  parseJSON (String s) = case s of
                          "snapshot" -> return Snapshot
                          "temporary" -> return Temporary
                          "backup" -> return Backup
                          _        -> fail $ "cannot parse " <> unpack s
  parseJSON e          = failParse e

-- | Type of droplet images
--
-- https://developers.digitalocean.com/documentation/v2/#images
data Image = Image { imageId          :: Id
                   , imageName        :: String
                   , distribution     :: String
                   , imageSlug        :: Maybe Slug
                   , publicImage      :: Bool
                   , imageRegions     :: [ Region ]
                   , min_disk_size    :: Bytes Giga
                   , image_created_at :: Date
                   , imageType        :: ImageType
                   } deriving Show

instance FromJSON Image where
  parseJSON (Object o) = Image
                         <$> o .: "id"
                         <*> o .: "name"
                         <*> o .: "distribution"
                         <*> o .:? "slug"
                         <*> o .: "public"
                         <*> o .: "regions"
                         <*> o .: "min_disk_size"
                         <*> o .: "created_at"
                         <*> o .: "type"
  parseJSON e          = failParse e

-- | Type of SSH @Key@s
--
--https://developers.digitalocean.com/documentation/v2/#ssh-keys
data Key = Key { keyId          :: Id
               , keyFingerprint :: String
               , publicKey      :: String
               , keyName        :: String
               } deriving Show

instance FromJSON Key where
  parseJSON (Object o) = Key
                         <$> o .: "id"
                         <*> o .: "fingerprint"
                         <*> o .: "public_key"
                         <*> o .: "name"
  parseJSON e          = failParse e

type TransferRate = Double
type Price = Double

-- | Type of Size objects
--
-- https://developers.digitalocean.com/documentation/v2/#sizes
data Size = Size { szSlug          :: SizeSlug
                 , szMemory        :: Bytes Mega
                 , szVcpus         :: Int
                 , szDisk          :: Bytes Giga
                 , szTransfer      :: TransferRate
                 , szPrice_Monthly :: Price
                 , szPrice_Hourly  :: Price
                 , szRegions       :: [ Region ]
                 , szAvailable     :: Bool
                 } deriving (Show)


instance FromJSON Size where
  parseJSON (Object o) = Size
                         <$> o .: "slug"
                         <*> o .: "memory"
                         <*> o .: "vcpus"
                         <*> o .: "disk"
                         <*> o .: "transfer"
                         <*> o .: "price_monthly"
                         <*> o .: "price_hourly"
                         <*> o .: "regions"
                         <*> o .: "available"

  parseJSON e          = failParse e


-- * Droplets Actions

-- | Type of action status
-- This is returned when action is initiated or when status of some action is requested

data ActionResult = ActionResult { actionId           :: Id
                                 , actionStatus       :: ActionStatus
                                 , actionType         :: ActionType
                                 , actionStartedAt    :: Maybe Date
                                 , actionCompletedAt  :: Maybe Date
                                 , actionResourceId   :: Id
                                 , actionResourceType :: String
                                 , actionRegionSlug   :: Region
                                 } deriving (Show)

instance FromJSON ActionResult where
  parseJSON (Object o) = ActionResult
                         <$> o .: "id"
                         <*> o .: "status"
                         <*> o .: "type"
                         <*> o .:? "started_at"
                         <*> o .:? "completed_at"
                         <*> o .: "resource_id"
                         <*> o .: "resource_type"
                         <*> o .: "region_slug"
  parseJSON v          = fail $ "cannot parse action " ++ show v

data ActionStatus = InProgress
                  | Completed
                  | Errored
                  deriving (Show)

instance FromJSON ActionStatus where
  parseJSON (String s) = case s of
                          "in-progress" -> return InProgress
                          "completed"   -> return Completed
                          "errored"     -> return Errored
                          _             -> fail $ "unknown action status " ++ show s
  parseJSON v          = fail $ "cannot parse action status " ++ show v

data ActionType = PowerOff
                | PowerOn
                deriving (Show)

instance FromJSON ActionType where
  parseJSON (String s) = case s of
                          "power_off" -> return PowerOff
                          "power_on"  -> return PowerOn
                          _           -> fail $ "unknown action type " ++ show s
  parseJSON v          = fail $ "cannot parse action type " ++ show v

instance ToJSON ActionType where
  toJSON PowerOff = String "power_off"
  toJSON PowerOn  = String "power_on"

data Action = DoPowerOff
            | DoPowerOn
            deriving Show

instance ToJSON Action where
  toJSON DoPowerOff = object [ "type" .= PowerOff ]
  toJSON DoPowerOn  = object [ "type" .= PowerOn ]

-- |Type of Domain zones
--
-- https://developers.digitalocean.com/documentation/v2/#domains
data Domain = Domain { domainName :: String
                     , domainTTL  :: Int
                     , zone_file  :: String
                     } deriving (Show)

instance FromJSON Domain where
  parseJSON (Object o) = Domain
                         <$> o .: "name"
                         <*> o .: "ttl"
                         <*> o .: "zone_file"

  parseJSON e          = failParse e

-- | Enumeration of possible DNS records types
data DNSType = A | CNAME | TXT | PTR | SRV | NS | AAAA | MX
             deriving (Show, Read, Generic)

instance FromJSON DNSType
instance ToJSON DNSType

-- | Type of Domain zone file entries
--
-- https://developers.digitalocean.com/documentation/v2/#domain-records
data DomainRecord = DomainRecord { recordId       :: Id
                                 , recordType     :: DNSType
                                 , recordName     :: String
                                 , recordData     :: String
                                 , recordPriority :: Maybe Double
                                 , recordPort     :: Maybe Int
                                 , recordWeight   :: Maybe Double
                                 } deriving (Show)


instance FromJSON DomainRecord where
  parseJSON (Object o) = DomainRecord
                         <$> o .: "id"
                         <*> o .: "type"
                         <*> o .: "name"
                         <*> o .: "data"
                         <*> o .: "priority"
                         <*> o .: "port"
                         <*> o .: "weight"

  parseJSON e          = failParse e

failParse :: (Show a1, Monad m) => a1 -> m a
failParse e = fail $ "cannot parse " <> show e