-- | Header used by all TUF types
module Hackage.Security.TUF.Header (
    HasHeader(..)
  , FileVersion(..)
  , FileExpires(..)
  , Header(..)
    -- ** Utility
  , expiresInDays
  , expiresNever
  , isExpired
  , versionInitial
  , versionIncrement
  ) where

import Data.Time
import Data.Typeable (Typeable)

import Hackage.Security.JSON
import Hackage.Security.Util.Lens

{-------------------------------------------------------------------------------
  TUF header
-------------------------------------------------------------------------------}

class HasHeader a where
  -- | File expiry date
  fileExpires :: Lens' a FileExpires

  -- | File version (monotonically increasing counter)
  fileVersion :: Lens' a FileVersion

-- | File version
--
-- The file version is a flat integer which must monotonically increase on
-- every file update.
--
-- 'Show' and 'Read' instance are defined in terms of the underlying 'Int'
-- (this is use for example by hackage during the backup process).
newtype FileVersion = FileVersion Int54
  deriving (Eq, Ord, Typeable)

instance Show FileVersion where
  show (FileVersion v) = show v

instance Read FileVersion where
  readsPrec p = map (\(v, xs) -> (FileVersion v, xs)) . readsPrec p

-- | File expiry date
--
-- A 'Nothing' value here means no expiry. That makes it possible to set some
-- files to never expire. (Note that not having the Maybe in the type here still
-- allows that, because you could set an expiry date 2000 years into the future.
-- By having the Maybe here we avoid the _need_ for such encoding issues.)
newtype FileExpires = FileExpires (Maybe UTCTime)
  deriving (Eq, Ord, Show, Typeable)

-- | Occassionally it is useful to read only a header from a file.
--
-- 'HeaderOnly' intentionally only has a 'FromJSON' instance (no 'ToJSON').
data Header = Header {
    headerExpires :: FileExpires
  , headerVersion :: FileVersion
  }

instance HasHeader Header where
  fileExpires f x = (\y -> x { headerExpires = y }) <$> f (headerExpires x)
  fileVersion f x = (\y -> x { headerVersion = y }) <$> f (headerVersion x)

{-------------------------------------------------------------------------------
  Utility
-------------------------------------------------------------------------------}

expiresNever :: FileExpires
expiresNever = FileExpires Nothing

expiresInDays :: UTCTime -> Integer -> FileExpires
expiresInDays now n =
    FileExpires . Just $ addUTCTime (fromInteger n * oneDay) now

isExpired :: UTCTime -> FileExpires -> Bool
isExpired _   (FileExpires Nothing)  = False
isExpired now (FileExpires (Just e)) = e < now

versionInitial :: FileVersion
versionInitial = FileVersion 1

versionIncrement :: FileVersion -> FileVersion
versionIncrement (FileVersion i) = FileVersion (i + 1)

{-------------------------------------------------------------------------------
  JSON
-------------------------------------------------------------------------------}

instance Monad m => ToJSON m FileVersion where
  toJSON (FileVersion i) = toJSON i

instance Monad m => ToJSON m FileExpires where
  toJSON (FileExpires (Just e)) = toJSON e
  toJSON (FileExpires Nothing)  = return JSNull

instance ReportSchemaErrors m => FromJSON m FileVersion where
  fromJSON enc = FileVersion <$> fromJSON enc

instance ReportSchemaErrors m => FromJSON m FileExpires where
  fromJSON JSNull = return $ FileExpires Nothing
  fromJSON enc    = FileExpires . Just <$> fromJSON enc

instance ReportSchemaErrors m => FromJSON m Header where
  fromJSON enc = do
    headerExpires <- fromJSField enc "expires"
    headerVersion <- fromJSField enc "version"
    return Header{..}

{-------------------------------------------------------------------------------
  Auxiliary
-------------------------------------------------------------------------------}

oneDay :: NominalDiffTime
oneDay = 24 * 60 * 60