-- Types.hs: hOpenPGP key tool lint subcommand types
-- Copyright © 2013-2026  Clint Adams
--
-- vim: softtabstop=4:shiftwidth=4:expandtab
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}

module HOpenPGP.Tools.Hokey.Lint.Types
    ( Color (..)
    , Result (..)
    , KAS (..)
    , LintContext (..)
    , KeyReport (..)
    , UIDReport (..)
    , SubkeyReport (..)
    , SubkeyRevocationDigestWarning (..)
    , CrossCertReport (..)
    , RevocationStatus (..)
    , colored
    , withColor
    , getResult
    , populateBestOf
    , best
    , bestOfRank
    , justTheUIDRs
    ) where

import Codec.Encryption.OpenPGP.Types
    ( Fingerprint
    , HashAlgorithm
    , KeyFlag
    , KeyVersion
    , PubKeyAlgorithm
    , SomePKPayload
    , SomeTK (..)
    , TK (..)
    , TKKind (..)
    , ThirtyTwoBitDuration
    , ThirtyTwoBitTimeStamp
    )
import Data.Aeson (ToJSON)
import Data.List (sortOn)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Text (Text)
import Data.Time.Clock.POSIX (POSIXTime)
import GHC.Generics

data Color
    = Green
    | Yellow
    | Red
    deriving (Eq, Generic, Ord)

data Result a = Result
    { resultColor :: Maybe Color
    , resultFindings :: Maybe [String]
    , resultValue :: a
    }
    deriving (Functor, Generic)

instance Applicative Result where
    pure x = Result Nothing Nothing x
    (Result c1 e1 f) <*> (Result c2 e2 x) =
        Result (max c1 c2) (e1 <> e2) (f x)

instance Monad Result where
    (Result c1 e1 x) >>= f =
        let Result c2 e2 y = f x
         in Result (max c1 c2) (e1 <> e2) y

colored :: Maybe Color -> Maybe [String] -> a -> Result a
colored c e x = Result c e x

withColor :: Maybe Color -> a -> Result a
withColor c x = Result c Nothing x

getResult :: Result a -> a
getResult (Result _ _ x) = x

data KAS
    = KAS
    { pubkeyalgo :: Result PubKeyAlgorithm
    , pubkeysize :: Result (Maybe Int)
    , stringrep :: String
    }
    deriving (Generic)

instance ToJSON KAS

instance ToJSON Color

instance (ToJSON a) => ToJSON (Result a)

data KeyReport
    = KeyReport
    { keyStatus :: Result String
    , keyFingerprint :: Result Fingerprint
    , keyVer :: Result KeyVersion
    , keyCreationTime :: Result ThirtyTwoBitTimeStamp
    , keyAlgorithmAndSize :: Result KAS
    , keyUIDsAndUAts :: Result (Map.Map Text (Result UIDReport))
    , keyBestOf :: Result (Maybe UIDReport)
    , keySubkeys :: Result [Result SubkeyReport]
    , keyHasEncryptionCapableSubkey :: Result Bool
    }
    deriving (Generic)

data UIDReport
    = UIDReport
    { uidSelfSigHashAlgorithms :: [Result HashAlgorithm]
    , uidPreferredHashAlgorithms :: [Result [HashAlgorithm]]
    , uidKeyExpirationTimes :: [Result [ThirtyTwoBitDuration]]
    , uidKeyUsageFlags :: [Result (Set.Set KeyFlag)]
    , uidRevocationStatus :: [RevocationStatus]
    }
    deriving (Generic)

data SubkeyReport
    = SubkeyReport
    { skFingerprint :: Result Fingerprint
    , skVer :: Result KeyVersion
    , skCreationTime :: ThirtyTwoBitTimeStamp
    , skAlgorithmAndSize :: Result KAS
    , skBindingSigHashAlgorithms :: [Result HashAlgorithm]
    , skRevocationSigWeakDigests :: [SubkeyRevocationDigestWarning]
    , skUsageFlags :: [Result (Set.Set KeyFlag)]
    , skCrossCerts :: CrossCertReport
    }
    deriving (Generic)

data SubkeyRevocationDigestWarning
    = SubkeyRevocationDigestWarning
    { srwHashAlgorithm :: HashAlgorithm
    , srwSubkeyFingerprint :: String
    , srwSubkeyKeyID :: Maybe String
    , srwMessage :: String
    }
    deriving (Generic)

data CrossCertReport
    = CrossCertReport
    { ccPresent :: Result Bool
    , ccHashAlgorithms :: [Result HashAlgorithm]
    }
    deriving (Generic)

data RevocationStatus
    = RevocationStatus
    { isRevoked :: Bool
    , revocationCode :: String
    , revocationReason :: Text
    }
    deriving (Generic)

instance ToJSON KeyReport

instance ToJSON UIDReport

instance ToJSON SubkeyReport

instance ToJSON SubkeyRevocationDigestWarning

instance ToJSON CrossCertReport

instance ToJSON RevocationStatus

instance Semigroup UIDReport where
    (<>) (UIDReport a b c d e) (UIDReport a' b' c' d' e') =
        UIDReport (a <> a') (b <> b') (c <> c') (d <> d') (e <> e')

instance Monoid UIDReport where
    mempty = UIDReport [] [] [] [] []
    mappend = (<>)

data LintContext = LintContext
    { lcMpt :: Maybe POSIXTime
    , lcProcessedTK :: TK 'PublicTK
    , lcProcResult :: Either String SomeTK
    , lcPrimaryKey :: SomePKPayload
    , lcFingerprint :: Fingerprint
    }

populateBestOf
    :: Map.Map Text (Result UIDReport) -> Maybe UIDReport
populateBestOf um
    | Map.null um = Nothing
    | otherwise =
        Just
            ( UIDReport
                <$> best . uidSelfSigHashAlgorithms
                <*> best
                    . uidPreferredHashAlgorithms
                <*> best
                    . uidKeyExpirationTimes
                <*> best
                    . uidKeyUsageFlags
                <*> pure []
                $ mconcat (justTheUIDRs um)
            )

justTheUIDRs :: Map.Map Text (Result UIDReport) -> [UIDReport]
justTheUIDRs = map getResult . Map.elems

best :: [Result a] -> [Result a]
best = take 1 . sortOn (bestOfRank . resultColor)

bestOfRank :: Maybe Color -> Int
bestOfRank (Just Green) = 0
bestOfRank (Just Yellow) = 1
bestOfRank (Just Red) = 2
bestOfRank Nothing = 3
