-- HKP.hs: hOpenPGP key tool
-- Copyright © 2016-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 OverloadedStrings #-}

module HOpenPGP.Tools.Common.HKP
    ( fetchKeys
    , FetchValidationMethod (..)
    , rearmorKeys
    ) where

import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA
import Codec.Encryption.OpenPGP.ASCIIArmor.Types
    ( Armor (Armor)
    , ArmorType (ArmorPublicKeyBlock)
    )
import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)
import Codec.Encryption.OpenPGP.Types
    ( Block (..)
    , Fingerprint
    , SomePKPayload (..)
    , SomeTK (..)
    , TK (..)
    , keyPktPKPayload
    , someTKToPublicViewTK
    , someTKToUnknown
    )
import Control.Arrow ((&&&))
import Control.Lens ((^..))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT (..), throwE)
import Data.Binary (get, put)
import Data.Binary.Put (runPut)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC8
import qualified Data.ByteString.Lazy as BL
import Data.Conduit (runConduitRes, (.|))
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL
import Data.Conduit.OpenPGP.Keyring
    ( conduitDropErrorsAndNothings
    , conduitToSomeTKsDroppingEither
    )
import Data.Conduit.Serialization.Binary (conduitGet)
import Data.Data.Lens (biplate)
import Data.Either (rights)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Network.HTTP.Client
    ( Response (..)
    , httpLbs
    , newManager
    , parseUrlThrow
    , setQueryString
    )
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Types.Status (ok200)
import Prettyprinter (pretty)

import HOpenPGP.Tools.Common.TKUtils (processTK)

primaryPKP :: SomeTK -> SomePKPayload
primaryPKP = keyPktPKPayload . _tkPrimaryKey . someTKToPublicViewTK

data FetchValidationMethod
    = MatchPrimaryKeyFingerprint
    | MatchPrimaryOrAnySubkeyFingerprint
    | AnySelfSigned
    deriving (Bounded, Enum, Eq, Read, Show)

fetchKeys
    :: String
    -> FetchValidationMethod
    -> Fingerprint
    -> ExceptT String IO [SomeTK]
fetchKeys ks fvm q = do
    manager <- liftIO $ newManager tlsManagerSettings
    request <- liftIO $ parseUrlThrow (ks <> basereq)
    let newreq = setQueryString (newqs q) request
    response <- liftIO $ httpLbs newreq manager
    processedKeys <-
        if responseStatus response == ok200
            then validateKeys (responseBody response)
            else throwE ("HTTP status: " ++ show (responseStatus response))
    return $
        map fst $
            filter (fvp fvm . primaryPKP . snd) processedKeys
  where
    fvp MatchPrimaryKeyFingerprint k = fingerprint k == q
    fvp MatchPrimaryOrAnySubkeyFingerprint k' =
        any (\k'' -> fingerprint k'' == q) (k' ^.. biplate)
    fvp AnySelfSigned _ = True
    basereq = "/pks/lookup"
    newqs q' =
        [ ("op", Just "get")
        , ("options", Just "mr")
        , ("exact", Just "on")
        , ("search", Just (BC8.pack ("0x" <> show (pretty q')))) -- FIXME: butter
        ]

validateKeys
    :: BL.ByteString -> ExceptT String IO [(SomeTK, SomeTK)] -- FIXME: conduit fail
validateKeys larmors = do
    bytestrings <-
        ExceptT $
            return $
                fmap (mconcat . map armorToBS) (AA.decodeLazy larmors)
    keys <-
        liftIO . runConduitRes $
            CB.sourceLbs bytestrings
                .| conduitGet get
                .| conduitToSomeTKsDroppingEither
                .| conduitDropErrorsAndNothings
                .| CL.consume
    cpt <- liftIO getPOSIXTime
    return . rights $
        map (uncurry (liftA2 (,)) . (pure &&& processTK (Just cpt))) keys
  where
    armorToBS (Armor ArmorPublicKeyBlock _ bs) = bs
    armorToBS _ = mempty

rearmorKeys :: [SomeTK] -> B.ByteString
rearmorKeys stks =
    if null stks
        then mempty
        else
            AA.encode
                . return
                . Armor ArmorPublicKeyBlock [("Comment", "filtered by hokey")]
                . runPut
                . put
                . Block
                $ map someTKToUnknown stks
