{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module WikiMusic.PostgreSQL.ArtistQuery () where

import Control.Concurrent.Async
import Data.Map (elems, keys)
import Data.Map qualified as Map
import Data.Text (pack)
import Hasql.Decoders as D
import Hasql.Encoders as E
import Hasql.Session qualified as Session
import Hasql.Statement (Statement (..))
import WikiMusic.Free.ArtistQuery
import WikiMusic.Model.Artist
import WikiMusic.Model.Artwork
import WikiMusic.Model.Comment
import WikiMusic.Model.Opinion
import WikiMusic.Model.Other
import WikiMusic.Model.Thread as CommentThread
import WikiMusic.PostgreSQL.Model.Artist
import WikiMusic.PostgreSQL.ReadAbstraction qualified as ReadAbstraction
import WikiMusic.Protolude

instance Exec ArtistQuery where
  execAlgebra (FetchArtists env sortOrder limit offset next) =
    next =<< fetchArtists' env sortOrder limit offset
  execAlgebra (FetchArtistsByUUID env sortOrder identifiers next) =
    next =<< fetchArtistsByUUID' env sortOrder identifiers
  execAlgebra (EnrichedArtistResponse env artistMap enrichArtistParams next) =
    next =<< enrichedArtistResponse' env artistMap enrichArtistParams
  execAlgebra (FetchArtistComments env identifiers next) =
    next =<< fetchArtistComments' env identifiers
  execAlgebra (FetchArtistOpinions env identifiers next) =
    next =<< fetchArtistOpinions' env identifiers
  execAlgebra (SearchArtists env searchInput sortOrder limit offset next) =
    next =<< searchArtists' env searchInput sortOrder limit offset
  execAlgebra (FetchArtistArtworks env identifiers next) =
    next =<< fetchArtistArtworks' env identifiers

-- | PostgreSQL artist row Hasql decoder for reading from `artists` table
artistRowDecoder :: Result [ArtistRow]
artistRowDecoder =
  D.rowList $
    (,,,,,,,,,,,,)
      <$> D.column (D.nonNullable D.uuid)
      <*> D.column (D.nonNullable D.text)
      <*> D.column (D.nonNullable D.uuid)
      <*> D.column (D.nonNullable D.int8)
      <*> D.column (D.nullable D.uuid)
      <*> D.column (D.nonNullable D.timestamptz)
      <*> D.column (D.nullable D.timestamptz)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nonNullable D.int8)
      <*> D.column (D.nullable D.text)

-- | Parse a PostgreSQL Hasql row to a domain representation of artist
parseArtistRows :: [ArtistRow] -> [(UUID, Artist)]
parseArtistRows = map parser
  where
    parser row = let artist = artistFromRow row in (artist ^. #identifier, artist)

-- | Fetch artists from storage, according to a sort order, limit and offset
fetchArtists' :: (MonadIO m) => Env -> ArtistSortOrder -> Limit -> Offset -> m (Map UUID Artist, [UUID])
fetchArtists' env sortOrder (Limit limit) (Offset offset) =
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement (fromIntegral limit, fromIntegral offset) stmt)
    (\xs -> (fromList xs, map fst xs))
    parseArtistRows
  where
    stmt = Statement query encoder artistRowDecoder True
    sortOrder' = fromString . WikiMusic.Model.Artist.show $ sortOrder
    query =
      encodeUtf8
        [untrimming|
        SELECT artists.identifier, artists.display_name, artists.created_by,
        artists.visibility_status, artists.approved_by, artists.created_at, artists.last_edited_at,
        spotify_url, youtube_url, soundcloud_url, wikipedia_url, artists.views, artists.description
        FROM artists
        LEFT JOIN artist_external_sources ON artist_external_sources.artist_identifier = artists.identifier
        ORDER BY $sortOrder'
        LIMIT ($$1) OFFSET $$2
      |]
    encoder =
      contrazip2
        (E.param . E.nonNullable $ E.int8)
        (E.param . E.nonNullable $ E.int8)

-- | Fetch artists by UUID from storage, according to a sort order
fetchArtistsByUUID' :: (MonadIO m) => Env -> ArtistSortOrder -> [UUID] -> m (Map UUID Artist, [UUID])
fetchArtistsByUUID' env sortOrder identifiers =
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement identifiers stmt)
    (\xs -> (fromList xs, map fst xs))
    parseArtistRows
  where
    stmt = Statement query encoder artistRowDecoder True
    sortOrder' = fromString . WikiMusic.Model.Artist.show $ sortOrder
    query =
      encodeUtf8
        [untrimming|
        SELECT artists.identifier, artists.display_name, artists.created_by,
        artists.visibility_status, artists.approved_by, artists.created_at, artists.last_edited_at,
        spotify_url, youtube_url, soundcloud_url, wikipedia_url, artists.views, artists.description 
        FROM artists 
        LEFT JOIN artist_external_sources ON artist_external_sources.artist_identifier = artists.identifier
        WHERE artists.identifier = ANY($$1) ORDER BY $sortOrder'            
      |]
    encoder = E.param . E.nonNullable $ E.foldableArray . E.nonNullable $ E.uuid

-- | Fetch artist artworks from storage
fetchArtistArtworks' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID ArtistArtwork)
fetchArtistArtworks' env identifiers =
  ReadAbstraction.fetchArtworks
    (env ^. #pool)
    (parseArtworkRows fromRow)
    "artist_artworks"
    "artist_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        artistIdentifier,
        createdBy,
        visibilityStatus,
        approvedBy,
        contentUrl,
        contentCaption,
        createdAt,
        lastEditedAt,
        orderValue
        ) =
        let artwork =
              Artwork
                { identifier = identifier,
                  createdBy = createdBy,
                  visibilityStatus = fromIntegral visibilityStatus,
                  approvedBy = approvedBy,
                  contentUrl = contentUrl,
                  contentCaption = contentCaption,
                  createdAt = createdAt,
                  lastEditedAt = lastEditedAt,
                  orderValue = fromIntegral orderValue
                }
         in ArtistArtwork {..}

-- | Enrich artists with related information, according to enrichment parameters
enrichedArtistResponse' :: (MonadIO m) => Env -> Map UUID Artist -> EnrichArtistParams -> m (Map UUID Artist)
enrichedArtistResponse' env artistMap enrichArtistParams = do
  (artworkMap, (opinionMap, commentMap)) <- liftIO $ concurrently getArtwork (concurrently getOpinion getComment)
  let enrichedArtists =
        mapMap
          ( \artist -> do
              let rawCommentMap = Map.filter (matchesArtistIdentifier artist) commentMap
                  allComments = elems rawCommentMap
                  commentThreads = map renderThread $ mkThreads allComments isChildOf' (^. #comment % #parentIdentifier)

              artist
                { comments = commentThreads,
                  artworks = filterMap (matchesArtistIdentifier artist) artworkMap,
                  opinions = filterMap (matchesArtistIdentifier artist) opinionMap
                }
          )
          artistMap

  pure enrichedArtists
  where
    matchesArtistIdentifier artist = (== artist ^. #identifier) . (^. #artistIdentifier)
    isChildOf' p x = Just (p ^. #comment % #identifier) == x ^. #comment % #parentIdentifier
    artistIds = keys artistMap
    getComment =
      if enrichArtistParams ^. #includeComments
        then exec @ArtistQuery $ fetchArtistComments env artistIds
        else pure $ fromList []
    getArtwork =
      if enrichArtistParams ^. #includeArtworks
        then exec @ArtistQuery $ fetchArtistArtworks env artistIds
        else pure $ fromList []
    getOpinion =
      if enrichArtistParams ^. #includeOpinions
        then exec @ArtistQuery $ fetchArtistOpinions env artistIds
        else pure $ fromList []

-- | Fetch artist comments from storage
fetchArtistComments' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID ArtistComment)
fetchArtistComments' env identifiers =
  ReadAbstraction.fetchComments
    (env ^. #pool)
    (parseCommentRows fromRow)
    "artist_comments"
    "artist_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        artistIdentifier,
        parentIdentifier,
        createdBy,
        visibilityStatus,
        contents,
        approvedBy,
        createdAt,
        lastEditedAt
        ) =
        let comment =
              Comment
                { identifier = identifier,
                  parentIdentifier = parentIdentifier,
                  createdBy = createdBy,
                  visibilityStatus = fromIntegral visibilityStatus,
                  contents = contents,
                  approvedBy = approvedBy,
                  createdAt = createdAt,
                  lastEditedAt = lastEditedAt
                }
         in ArtistComment {..}

-- | Search artists by keywords from storage, according to a sort order, limit and offset
searchArtists' :: (MonadIO m) => Env -> SearchInput -> ArtistSortOrder -> Limit -> Offset -> m (Map UUID Artist, [UUID])
searchArtists' env searchInput sortOrder (Limit limit) (Offset offset) =
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement (fromIntegral limit, fromIntegral offset) stmt)
    (\xs -> (fromList xs, map fst xs))
    parseArtistRows
  where
    sortOrder' = pack . WikiMusic.Model.Artist.show $ sortOrder
    searchConstraints = ReadAbstraction.mkSearchConstraints (searchInput ^. #value)
    query =
      encodeUtf8
        [untrimming|
          SELECT artists.identifier, artists.display_name, artists.created_by, 
          artists.visibility_status, artists.approved_by, artists.created_at, artists.last_edited_at, 
          spotify_url, youtube_url, soundcloud_url, wikipedia_url, artists.views, artists.description 
          FROM artists 
          LEFT JOIN artist_external_sources ON artist_external_sources.artist_identifier = artists.identifier 
          WHERE $searchConstraints
          ORDER BY $sortOrder'
          LIMIT ($$1) OFFSET $$2
        |]

    encoder =
      contrazip2
        (E.param . E.nonNullable $ E.int8)
        (E.param . E.nonNullable $ E.int8)
    stmt = Statement query encoder artistRowDecoder True

-- | Fetch artist opinions from storage
fetchArtistOpinions' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID ArtistOpinion)
fetchArtistOpinions' env identifiers =
  ReadAbstraction.fetchOpinions
    (env ^. #pool)
    (parseOpinionRows fromRow)
    "artist_opinions"
    "artist_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        artistIdentifier,
        createdBy,
        isLike,
        isDislike,
        createdAt,
        lastEditedAt
        ) =
        let opinion = Opinion {..}
         in ArtistOpinion {..}
