{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE PolyKinds                  #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeOperators              #-}

module Servant.Seo.Robots where

import           Control.Lens
import           Data.Coerce             (coerce)
import           Data.Kind               (Type)
import           Data.Text               (Text)
import qualified Data.Text               as Text
import           GHC.Generics            (Generic)
import           GHC.TypeLits            (KnownNat, KnownSymbol, Nat, symbolVal)
import           Servant
import           Text.Blaze              (ToMarkup)

import           Servant.Seo.Combinators

-- * Robots.txt

-- ** RobotsInfo

-- | Intermediate structure representing @robots.txt@ file.
-- All API parts marked as 'Disallow' would be aggregated into 'RobotsInfo' during compilation and translated to @robots.txt@ content.
data RobotsInfo = RobotsInfo
    { _robotsSitemapPath     :: Maybe () -- ^ Indicate whether sitemap should be present in robots file or not.
    , _robotsDisallowedPaths :: [DisallowedPathPiece] -- ^ Path pieces that should be present in robots file.
    }
    deriving (Show, Eq, Generic)

-- | Part of URL that should be present in robots file.
newtype DisallowedPathPiece = DisallowedPathPiece Text
  deriving stock (Show, Eq, Generic)
  deriving newtype (Ord, ToMarkup)

makeLenses ''RobotsInfo

-- | Empty unit of 'RobotsInfo'.
instance Monoid RobotsInfo where
  mempty = RobotsInfo Nothing []

instance Semigroup RobotsInfo where
  RobotsInfo s1 d1 <> RobotsInfo s2 d2 = RobotsInfo (s1 <> s2) (d1 <> d2)

-- ** HasRobots

-- | Servant API extension.
-- It describes how to build 'RobotsInfo' from servant API.
-- Most of types add nothing to it.
class HasRobots a where
  toRobots :: Proxy a -> RobotsInfo

instance HasRobots Raw where
  toRobots _ = mempty

instance HasRobots EmptyAPI where
  toRobots _ = mempty

-- | Collect different path pieces.
instance (HasRobots a, HasRobots b) => HasRobots (a :<|> b) where
  toRobots _ = toRobots (Proxy :: Proxy a) <> toRobots (Proxy :: Proxy b)

instance HasRobots sub => HasRobots (WithNamedContext x c sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (HttpVersion :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (StreamBody' mods fr ct a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (ReqBody' mods cs a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (RemoteHost :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (QueryParam' mods sym a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (QueryParams sym a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (QueryFlag sym :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (Header' mods sym a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (IsSecure :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots api => HasRobots (Summary desc :> api) where
  toRobots _ = toRobots (Proxy :: Proxy api)

instance HasRobots api => HasRobots (Description desc :> api) where
  toRobots _ = toRobots (Proxy :: Proxy api)

instance (KnownSymbol sym, HasRobots sub) =>
  HasRobots (Capture' mods sym a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (CaptureAll sym a :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

instance HasRobots sub => HasRobots (Vault :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub)

-- | Append path piece to existing 'DisallowedPathPiece'.
instance (HasRobots sub, KnownSymbol sym) => HasRobots (sym :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub) & decide piece
    where
      piece = (Text.append "/" . Text.pack . symbolVal) (Proxy :: Proxy sym)
      decide x ys@RobotsInfo{..} = case _robotsDisallowedPaths of
        [] | piece /= "/sitemap.xml" -> ys
           | otherwise -> ys & robotsSitemapPath ?~ ()
        _  -> ys & robotsDisallowedPaths .~ (DisallowedPathPiece .  Text.append x . coerce <$> (ys ^. robotsDisallowedPaths))

-- | Generate new 'DisallowedPathPiece' from path piece marked as 'Disallow'.
instance (HasRobots sub, KnownSymbol sym) => HasRobots (Disallow sym :> sub) where
  toRobots _ = toRobots (Proxy :: Proxy sub) & addPath piece
    where
      piece = (DisallowedPathPiece . Text.append "/" . Text.pack . symbolVal) (Proxy :: Proxy sym)
      addPath x xs = RobotsInfo Nothing [x] <> xs

instance {-# OVERLAPPABLE #-} (KnownNat status) => HasRobots (Verb method status cs NoContent) where
  toRobots _ = mempty

instance {-# OVERLAPPABLE #-} (KnownNat status) =>
  HasRobots (Verb method status cs a) where
    toRobots _ = mempty

instance {-# OVERLAPPABLE #-} (KnownNat status) => HasRobots (Verb (method :: Type) (status :: Nat) (cs :: [Type]) (Headers hs NoContent)) where
  toRobots _ = mempty

instance {-# OVERLAPPABLE #-} ( KnownNat status) => HasRobots (Verb (method :: Type) (status :: Nat) (cs :: [Type]) (Headers hs a)) where
  toRobots _ = mempty

-- | 'Frequency' as part of @sitemap.xml@ spec has no impact on @robots.txt@.
instance (HasPeriod period, HasRobots api) => HasRobots (Frequency period :> api) where
  toRobots _ = toRobots (Proxy :: Proxy api)

-- | 'Priority' as part of @sitemap.xml@ spec has no impact on @robots.txt@.
instance (HasRobots api) =>
  HasRobots (Priority priority :> api) where
    toRobots _ = toRobots (Proxy :: Proxy api)