packages feed

todoist-sdk-0.1.2.1: src/Web/Todoist/Runner/IO/Interpreters.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

-- | TodoistIO interpreter implementations for all domain type classes.
-- This module contains all the orphan instances that implement the domain
-- operations using actual HTTP requests to the Todoist API.
module Web.Todoist.Runner.IO.Interpreters
    ( -- * Conversion Functions (for testing)
      projectResponseToProject
    , commentResponseToComment
    ) where

import Control.Applicative (pure, (<$>))
import Control.Monad (fmap)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT, except)
import Control.Monad.Trans.Reader (ReaderT, ask)
import Data.Either (Either (Left, Right))
import Data.Function (($))
import Data.Int (Int)
import Data.Maybe (Maybe (..), fromMaybe)
import Data.Monoid ((<>))
import Data.Proxy (Proxy (Proxy))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Traversable (traverse)
import Data.Void (Void)
import System.IO (IO)

-- Domain imports
import Web.Todoist.Domain.Comment
    ( Comment (..)
    , CommentCreate
    , CommentId (..)
    , CommentParam (..)
    , CommentUpdate
    , TodoistCommentM (..)
    )
import Web.Todoist.Domain.Label
    ( Label (..)
    , LabelCreate
    , LabelId (..)
    , LabelParam (..)
    , LabelUpdate
    , SharedLabelParam (..)
    , SharedLabelRemove
    , SharedLabelRename
    , TodoistLabelM (..)
    )
import Web.Todoist.Domain.Project
    ( CanAssignTasks (..)
    , Collaborator
    , IsArchived (..)
    , IsShared (..)
    , PaginationParam (..)
    , Project (..)
    , ProjectCreate
    , ProjectUpdate
    , TodoistProjectM (..)
    )
import Web.Todoist.Domain.Section
    ( Section (..)
    , SectionCreate
    , SectionId (..)
    , SectionParam (..)
    , SectionUpdate
    , TodoistSectionM (..)
    )
import Web.Todoist.Domain.Task
    ( AddTaskQuick
    , CompletedTasksQueryParam
    , CompletedTasksQueryParamAPI (items)
    , Deadline (..)
    , Due (..)
    , Duration (..)
    , DurationUnit (..)
    , MoveTask
    , NewTask (..)
    , Task (..)
    , TaskCompletedItem (..)
    , TaskCreate
    , TaskFilter (..)
    , TaskParam (..)
    , TaskUpdate
    , TodoistTaskM (..)
    )
import Web.Todoist.Domain.Types
    ( Color (..)
    , Content (..)
    , Description (..)
    , IsFavorite (..)
    , IsCollapsed (..)
    , Name (..)
    , Order (..)
    , ParentId (..)
    , ProjectId (..)
    , TaskId (..)
    , Uid (..)
    , parseViewStyle
    )

-- Internal imports
import Web.Todoist.Internal.Config (TodoistConfig)
import Web.Todoist.Internal.Error (TodoistError (HttpError))
import Web.Todoist.Internal.HTTP (PostResponse (..), apiDelete, apiGet, apiPost)
import Web.Todoist.Internal.Request (mkTodoistRequest)
import Web.Todoist.Internal.Types
    ( CommentResponse (..)
    , CreatedAt (..)
    , DeadlineResponse (..)
    , DueResponse (..)
    , DurationResponse (..)
    , LabelResponse (..)
    , NewTaskResponse (..)
    , ProjectPermissions
    , ProjectResponse (..)
    , SectionResponse (..)
    , TaskResponse (..)
    , TodoistReturn (next_cursor, results)
    , UpdatedAt (..)
    )

-- Runner imports
import Web.Todoist.Util.QueryParam (QueryParam (toQueryParam))
import Web.Todoist.Runner.IO.Core (TodoistIO (..))

-- ============================================================================
-- Conversion Functions (Internal - Not Exported)
-- ============================================================================

-- | Convert ProjectResponse to Project
projectResponseToProject :: ProjectResponse -> Project
projectResponseToProject ProjectResponse {..} =
    let (CreatedAt createdAt) = p_created_at
        (UpdatedAt updatedAt) = p_updated_at
     in Project
            { _id = ProjectId p_id
            , _name = Name p_name
            , _description = Description p_description
            , _order = Order p_child_order
            , _color = Color p_color
            , _is_collapsed = IsCollapsed p_is_collapsed
            , _is_shared = IsShared p_is_shared
            , _is_favorite = IsFavorite p_is_favorite
            , _is_archived = IsArchived p_is_archived
            , _can_assign_tasks = CanAssignTasks p_can_assign_tasks
            , _view_style = parseViewStyle p_view_style
            , _created_at = createdAt
            , _updated_at = updatedAt
            }

-- | Parse duration unit text to DurationUnit enum
parseDurationUnit :: Text -> DurationUnit
parseDurationUnit txt = case T.toLower txt of
    "minute" -> Minute
    "day" -> Day
    _ -> Day -- Default to Day for unknown units

-- | Convert DurationResponse to Duration
durationResponseToDuration :: DurationResponse -> Duration
durationResponseToDuration DurationResponse {..} =
    Duration
        { _amount = p_amount
        , _unit = parseDurationUnit p_unit
        }

-- | Convert DeadlineResponse to Deadline
deadlineResponseToDeadline :: DeadlineResponse -> Deadline
deadlineResponseToDeadline DeadlineResponse {..} =
    Deadline
        { _date = p_date
        , _lang = p_lang
        }

-- | Convert DueResponse to Due
dueResponseToDue :: DueResponse -> Due
dueResponseToDue DueResponse {..} =
    Due
        { _date = p_date
        , _string = p_string
        , _lang = p_lang
        , _is_recurring = p_is_recurring
        , _timezone = p_timezone
        }

-- | Convert TaskResponse to Task
taskResponseToTask :: TaskResponse -> Task
taskResponseToTask TaskResponse {..} =
    Task
        { _id = TaskId p_id
        , _content = Content p_content
        , _description = Description p_description
        , _project_id = ProjectId p_project_id
        , _section_id = fmap (\sid -> SectionId {_id = sid}) p_section_id
        , _parent_id = fmap ParentId p_parent_id
        , _labels = p_labels
        , _priority = p_priority
        , _due = fmap dueResponseToDue p_due
        , _deadline = fmap deadlineResponseToDeadline p_deadline
        , _duration = fmap durationResponseToDuration p_duration
        , _is_collapsed = IsCollapsed p_is_collapsed
        , _order = Order p_child_order
        , _assignee_id = fmap Uid p_responsible_uid
        , _assigner_id = fmap Uid p_assigned_by_uid
        , _completed_at = p_completed_at
        , _creator_id = Uid p_user_id
        , _created_at = fromMaybe "" p_added_at
        , _updated_at = fromMaybe "" p_updated_at
        }

-- | Convert NewTaskResponse to NewTask
newTaskResponseToNewTask :: NewTaskResponse -> NewTask
newTaskResponseToNewTask NewTaskResponse {..} =
    NewTask
        { _user_id = p_user_id
        , _id = TaskId p_id
        , _project_id = ProjectId p_project_id
        , _section_id = fmap (\sid -> SectionId {_id = sid}) p_section_id
        , _parent_id = fmap ParentId p_parent_id
        , _added_by_uid = fmap Uid p_added_by_uid
        , _assigned_by_uid = fmap Uid p_assigned_by_uid
        , _responsible_uid = fmap Uid p_responsible_uid
        , _labels = p_labels
        , _checked = p_checked
        , _is_deleted = p_is_deleted
        , _added_at = p_added_at
        , _completed_at = p_completed_at
        , _updated_at = p_updated_at
        , _priority = p_priority
        , _child_order = Order p_child_order
        , _content = Content p_content
        , _description = Description p_description
        , _note_count = p_note_count
        , _day_order = Order p_day_order
        , _is_collapsed = IsCollapsed p_is_collapsed
        }

-- | Convert CommentResponse to Comment
-- Validates that at least one of task_id or project_id is present
commentResponseToComment :: CommentResponse -> Either TodoistError Comment
commentResponseToComment CommentResponse {..} =
    case (p_item_id, p_project_id) of
        (Nothing, Nothing) -> Left $ HttpError "Comment must have either task_id or project_id"
        _ ->
            Right $
                Comment
                    { _id = CommentId p_id
                    , _content = Content p_content
                    , _poster_id = fmap Uid p_posted_uid
                    , _posted_at = fmap Uid p_posted_at
                    , _task_id = fmap TaskId p_item_id
                    , _project_id = fmap ProjectId p_project_id
                    , _attachment = p_file_attachment
                    }

-- | Convert SectionResponse to Section
sectionResponseToSection :: SectionResponse -> Section
sectionResponseToSection SectionResponse {..} =
    Section
        { _id = SectionId {_id = p_id}
        , _name = Name p_name
        , _project_id = ProjectId p_project_id
        , _is_collapsed = IsCollapsed p_is_collapsed
        , _order = Order p_section_order
        }

-- | Convert LabelResponse to Label
labelResponseToLabel :: LabelResponse -> Label
labelResponseToLabel LabelResponse {..} =
    Label
        { _id = LabelId p_id
        , _name = Name p_name
        , _color = Color p_color
        , _order = fmap Order p_order
        , _is_favorite = IsFavorite p_is_favorite
        }

-- ============================================================================
-- TodoistProjectM Instance
-- ============================================================================

instance TodoistProjectM TodoistIO where
    getProject :: ProjectId -> TodoistIO Project
    getProject ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["projects", projectIdText] Nothing Nothing
        resp <- liftIO $ apiGet (Proxy @ProjectResponse) config apiRequest
        case resp of
            Right res -> pure $ projectResponseToProject res
            Left err -> lift $ except (Left err)

    getAllProjects :: TodoistIO [Project]
    getAllProjects = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [Project] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Project]
            loop cursor acc = do
                let params = PaginationParam {cursor = cursor, limit = Nothing}
                    apiRequest = mkTodoistRequest @Void ["projects"] (Just $ toQueryParam params) Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn ProjectResponse)) config apiRequest
                case resp of
                    Right res -> do
                        let newAcc = acc <> (projectResponseToProject <$> results res)
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    getProjectCollaborators :: ProjectId -> TodoistIO [Collaborator]
    getProjectCollaborators ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let loop ::
                Maybe Text -> [Collaborator] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Collaborator]
            loop cursor acc = do
                let params = PaginationParam {cursor = cursor, limit = Nothing}
                    apiRequest =
                        mkTodoistRequest @Void
                            ["projects", projectIdText, "collaborators"]
                            (Just $ toQueryParam params)
                            Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn Collaborator)) config apiRequest
                case resp of
                    Right res -> do
                        let newAcc = acc <> results res
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    deleteProject :: ProjectId -> TodoistIO ()
    deleteProject ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["projects", projectIdText] Nothing Nothing
        resp <- liftIO $ apiDelete config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    addProject :: ProjectCreate -> TodoistIO ProjectId
    addProject project = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @ProjectCreate ["projects"] Nothing Nothing
        resp <- liftIO $ apiPost (Just project) (JsonResponse (Proxy @ProjectId)) config apiRequest
        case resp of
            Right res -> pure res
            Left err -> lift $ except (Left err)

    archiveProject :: ProjectId -> TodoistIO ProjectId
    archiveProject ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["projects", projectIdText, "archive"] Nothing Nothing
        resp <- liftIO $ apiPost (Nothing @Void) (JsonResponse (Proxy @ProjectId)) config apiRequest
        case resp of
            Right res -> pure res
            Left err -> lift $ except (Left err)

    unarchiveProject :: ProjectId -> TodoistIO ProjectId
    unarchiveProject ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["projects", projectIdText, "unarchive"] Nothing Nothing
        resp <- liftIO $ apiPost (Nothing @Void) (JsonResponse (Proxy @ProjectId)) config apiRequest
        case resp of
            Right res -> pure res
            Left err -> lift $ except (Left err)

    getProjectPermissions :: TodoistIO ProjectPermissions
    getProjectPermissions = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["projects", "permissions"] Nothing Nothing
        resp <- liftIO $ apiGet (Proxy @ProjectPermissions) config apiRequest
        case resp of
            Right res -> pure res
            Left err -> lift $ except (Left err)

    updateProject :: ProjectUpdate -> ProjectId -> TodoistIO Project
    updateProject projectUpdate ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @ProjectUpdate ["projects", projectIdText] Nothing Nothing
        resp <-
            liftIO $ apiPost (Just projectUpdate) (JsonResponse (Proxy @ProjectResponse)) config apiRequest
        case resp of
            Right res -> pure $ projectResponseToProject res
            Left err -> lift $ except (Left err)

    getAllProjectsPaginated :: PaginationParam -> TodoistIO ([Project], Maybe Text)
    getAllProjectsPaginated params = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["projects"] (Just $ toQueryParam params) Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn ProjectResponse)) config apiRequest
        case resp of
            Right res ->
                pure (projectResponseToProject <$> results res, fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

    getProjectCollaboratorsPaginated ::
        PaginationParam -> ProjectId -> TodoistIO ([Collaborator], Maybe Text)
    getProjectCollaboratorsPaginated params ProjectId {getProjectId = projectIdText} = TodoistIO $ do
        config <- ask
        let apiRequest =
                mkTodoistRequest @Void
                    ["projects", projectIdText, "collaborators"]
                    (Just $ toQueryParam params)
                    Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn Collaborator)) config apiRequest
        case resp of
            Right res -> pure (results res, fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

    getAllProjectsWithLimit :: Int -> TodoistIO [Project]
    getAllProjectsWithLimit pageLimit = TodoistIO $ do
        let loop :: Maybe Text -> [Project] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Project]
            loop cursor acc = do
                let params = PaginationParam {cursor = cursor, limit = Just pageLimit}
                (projects, nextCursor) <- unTodoist $ getAllProjectsPaginated params
                let newAcc = acc <> projects
                case nextCursor of
                    Nothing -> pure newAcc
                    Just c -> loop (Just c) newAcc
        loop Nothing []

    getProjectCollaboratorsWithLimit :: Int -> ProjectId -> TodoistIO [Collaborator]
    getProjectCollaboratorsWithLimit pageLimit projectId = TodoistIO $ do
        let loop ::
                Maybe Text -> [Collaborator] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Collaborator]
            loop cursor acc = do
                let params = PaginationParam {cursor = cursor, limit = Just pageLimit}
                (collaborators, nextCursor) <- unTodoist $ getProjectCollaboratorsPaginated params projectId
                let newAcc = acc <> collaborators
                case nextCursor of
                    Nothing -> pure newAcc
                    Just c -> loop (Just c) newAcc
        loop Nothing []

-- ============================================================================
-- TodoistTaskM Instance
-- ============================================================================

instance TodoistTaskM TodoistIO where
    getTasks :: TaskParam -> TodoistIO [Task]
    getTasks initialParams = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [Task] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Task]
            loop cursorVal acc = do
                let TaskParam {project_id, section_id, parent_id, task_ids} = initialParams
                    params = TaskParam {project_id, section_id, parent_id, task_ids, cursor = cursorVal, limit = Nothing}
                    apiRequest = mkTodoistRequest @Void ["tasks"] (Just $ toQueryParam params) Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn TaskResponse)) config apiRequest
                case resp of
                    Right res -> do
                        let newAcc = acc <> fmap taskResponseToTask (results res)
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    getTask :: TaskId -> TodoistIO Task
    getTask TaskId {getTaskId = taskIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["tasks", taskIdText] Nothing Nothing
        resp <- liftIO $ apiGet (Proxy @TaskResponse) config apiRequest
        case resp of
            Right res -> pure (taskResponseToTask res)
            Left err -> lift $ except (Left err)

    createTask :: TaskCreate -> TodoistIO NewTask
    createTask taskCreate = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @TaskCreate ["tasks"] Nothing Nothing
        resp <- liftIO $ apiPost (Just taskCreate) (JsonResponse (Proxy @NewTaskResponse)) config apiRequest
        case resp of
            Right res -> pure (newTaskResponseToNewTask res)
            Left err -> lift $ except (Left err)

    updateTask :: TaskUpdate -> TaskId -> TodoistIO NewTask
    updateTask taskPatch TaskId {getTaskId = taskIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @TaskUpdate ["tasks", taskIdText] Nothing Nothing
        resp <- liftIO $ apiPost (Just taskPatch) (JsonResponse (Proxy @NewTaskResponse)) config apiRequest
        case resp of
            Right res -> pure (newTaskResponseToNewTask res)
            Left err -> lift $ except (Left err)

    closeTask :: TaskId -> TodoistIO ()
    closeTask TaskId {getTaskId = taskIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["tasks", taskIdText, "close"] Nothing Nothing
        resp <- liftIO $ apiPost (Nothing @Void) IgnoreResponse config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    uncloseTask :: TaskId -> TodoistIO ()
    uncloseTask TaskId {getTaskId = taskIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["tasks", taskIdText, "reopen"] Nothing Nothing
        resp <- liftIO $ apiPost (Nothing @Void) IgnoreResponse config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    deleteTask :: TaskId -> TodoistIO ()
    deleteTask TaskId {getTaskId = taskIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["tasks", taskIdText] Nothing Nothing
        resp <- liftIO $ apiDelete config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    getTasksByFilter :: TaskFilter -> TodoistIO [TaskId]
    getTasksByFilter initialFilter = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [TaskId] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [TaskId]
            loop cursorVal acc = do
                let TaskFilter {query, lang, limit} = initialFilter
                    filter' = TaskFilter {query, lang, cursor = cursorVal, limit}
                    apiRequest = mkTodoistRequest @Void ["tasks", "filter"] (Just $ toQueryParam filter') Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn TaskResponse)) config apiRequest
                case resp of
                    Right res -> do
                        let taskIds = fmap (\TaskResponse {p_id} -> TaskId {getTaskId = p_id}) (results res)
                        let newAcc = acc <> taskIds
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    moveTask :: MoveTask -> TaskId -> TodoistIO TaskId
    moveTask moveTaskBody TaskId {getTaskId = taskIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @MoveTask ["tasks", taskIdText, "move"] Nothing Nothing
        resp <- liftIO $ apiPost (Just moveTaskBody) (JsonResponse (Proxy @TaskResponse)) config apiRequest
        case resp of
            Right TaskResponse {p_id} -> pure $ TaskId {getTaskId = p_id}
            Left err -> lift $ except (Left err)

    addTaskQuick :: AddTaskQuick -> TodoistIO ()
    addTaskQuick atqBody = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @AddTaskQuick ["tasks", "quick"] Nothing Nothing
        resp <- liftIO $ apiPost (Just atqBody) IgnoreResponse config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    getCompletedTasksByDueDate :: CompletedTasksQueryParam -> TodoistIO [TaskId]
    getCompletedTasksByDueDate completedTasksQueryParam = TodoistIO $ do
        config <- ask
        let apiRequest =
                mkTodoistRequest @CompletedTasksQueryParam
                    ["tasks", "completed", "by_due_date"]
                    (Just $ toQueryParam completedTasksQueryParam)
                    Nothing
        resp <- liftIO $ apiGet (Proxy @CompletedTasksQueryParamAPI) config apiRequest
        case resp of
            Right res -> pure $ fmap (\(TaskCompletedItem tid) -> TaskId {getTaskId = tid}) (items res)
            Left err -> lift $ except (Left err)

    getCompletedTasksByCompletionDate :: CompletedTasksQueryParam -> TodoistIO [TaskId]
    getCompletedTasksByCompletionDate completedTasksQueryParam = TodoistIO $ do
        config <- ask
        let apiRequest =
                mkTodoistRequest @CompletedTasksQueryParam
                    ["tasks", "completed", "by_completion_date"]
                    (Just $ toQueryParam completedTasksQueryParam)
                    Nothing
        resp <- liftIO $ apiGet (Proxy @CompletedTasksQueryParamAPI) config apiRequest
        case resp of
            Right res -> pure $ fmap (\(TaskCompletedItem tid) -> TaskId {getTaskId = tid}) (items res)
            Left err -> lift $ except (Left err)

    getTasksPaginated :: TaskParam -> TodoistIO ([TaskId], Maybe Text)
    getTasksPaginated taskparams = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["tasks"] (Just $ toQueryParam taskparams) Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn TaskResponse)) config apiRequest
        case resp of
            Right res -> do
                let taskIds = fmap (\TaskResponse {p_id} -> TaskId {getTaskId = p_id}) (results res)
                pure (taskIds, fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

    getTasksByFilterPaginated :: TaskFilter -> TodoistIO ([TaskId], Maybe Text)
    getTasksByFilterPaginated taskFilter = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["tasks", "filter"] (Just $ toQueryParam taskFilter) Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn TaskResponse)) config apiRequest
        case resp of
            Right res -> do
                let taskIds = fmap (\TaskResponse {p_id} -> TaskId {getTaskId = p_id}) (results res)
                pure (taskIds, fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

    getTasksWithLimit :: Int -> TaskParam -> TodoistIO [TaskId]
    getTasksWithLimit pageLimit baseParams = TodoistIO $ do
        let loop :: Maybe Text -> [TaskId] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [TaskId]
            loop cursorVal acc = do
                let TaskParam {project_id, section_id, parent_id, task_ids} = baseParams
                    params =
                        TaskParam {project_id, section_id, parent_id, task_ids, cursor = cursorVal, limit = Just pageLimit}
                (tasks, nextCursor) <- unTodoist $ getTasksPaginated params
                let newAcc = acc <> tasks
                case nextCursor of
                    Nothing -> pure newAcc
                    Just c -> loop (Just c) newAcc
        loop Nothing []

    getTasksByFilterWithLimit :: TaskFilter -> Int -> TodoistIO [TaskId]
    getTasksByFilterWithLimit baseFilter pageLimit = TodoistIO $ do
        let loop :: Maybe Text -> [TaskId] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [TaskId]
            loop cursorVal acc = do
                let TaskFilter {query, lang} = baseFilter
                    filter' = TaskFilter {query, lang, cursor = cursorVal, limit = Just pageLimit}
                (tasks, nextCursor) <- unTodoist $ getTasksByFilterPaginated filter'
                let newAcc = acc <> tasks
                case nextCursor of
                    Nothing -> pure newAcc
                    Just c -> loop (Just c) newAcc
        loop Nothing []

-- ============================================================================
-- TodoistCommentM Instance
-- ============================================================================

instance TodoistCommentM TodoistIO where
    addComment :: CommentCreate -> TodoistIO Comment
    addComment comment = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @CommentCreate ["comments"] Nothing Nothing
        resp <- liftIO $ apiPost (Just comment) (JsonResponse (Proxy @CommentResponse)) config apiRequest
        case resp of
            Right res -> do
                let commentResults = commentResponseToComment res
                case commentResults of
                    Left err -> lift $ except (Left err)
                    Right cmnt -> pure cmnt
            Left err -> lift $ except (Left err)

    getComments :: CommentParam -> TodoistIO [Comment]
    getComments initialParams = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [Comment] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Comment]
            loop cursorVal acc = do
                let CommentParam {project_id, task_id, public_key} = initialParams
                    params = CommentParam {project_id, task_id, cursor = cursorVal, limit = Nothing, public_key}
                    apiRequest = mkTodoistRequest @Void ["comments"] (Just $ toQueryParam params) Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn CommentResponse)) config apiRequest
                case resp of
                    Right res -> do
                        let commentResults = traverse commentResponseToComment (results res)
                        case commentResults of
                            Left err -> lift $ except (Left err)
                            Right comments -> do
                                let newAcc = acc <> comments
                                case next_cursor res of
                                    Nothing -> pure newAcc
                                    Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    getCommentsPaginated :: CommentParam -> TodoistIO ([Comment], Maybe Text)
    getCommentsPaginated params = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["comments"] (Just $ toQueryParam params) Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn CommentResponse)) config apiRequest
        case resp of
            Right res -> do
                let commentResults = traverse commentResponseToComment (results res)
                    nextCursor = fmap T.pack (next_cursor res)
                case commentResults of
                    Left err -> lift $ except (Left err)
                    Right comments -> pure (comments, nextCursor)
            Left err -> lift $ except (Left err)

    getComment :: CommentId -> TodoistIO Comment
    getComment CommentId {getCommentId = commentIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["comments", commentIdText] Nothing Nothing
        resp <- liftIO $ apiGet (Proxy @CommentResponse) config apiRequest
        case resp of
            Right res ->
                case commentResponseToComment res of
                    Left err -> lift $ except (Left err)
                    Right comment -> pure comment
            Left err -> lift $ except (Left err)

    updateComment :: CommentUpdate -> CommentId -> TodoistIO Comment
    updateComment update CommentId {getCommentId = commentIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @CommentUpdate ["comments", commentIdText] Nothing Nothing
        resp <- liftIO $ apiPost (Just update) (JsonResponse (Proxy @CommentResponse)) config apiRequest
        case resp of
            Right res ->
                case commentResponseToComment res of
                    Left err -> lift $ except (Left err)
                    Right comment -> pure comment
            Left err -> lift $ except (Left err)

    deleteComment :: CommentId -> TodoistIO ()
    deleteComment CommentId {getCommentId = commentIdText} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["comments", commentIdText] Nothing Nothing
        resp <- liftIO $ apiDelete config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

-- ============================================================================
-- TodoistSectionM Instance
-- ============================================================================

instance TodoistSectionM TodoistIO where
    getSections :: SectionParam -> TodoistIO [Section]
    getSections initialParams = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [Section] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Section]
            loop cursorVal acc = do
                let SectionParam {project_id} = initialParams
                    params = SectionParam {project_id, cursor = cursorVal, limit = Nothing}
                    apiRequest = mkTodoistRequest @Void ["sections"] (Just $ toQueryParam params) Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn SectionResponse)) config apiRequest
                case resp of
                    Right res -> do
                        let newAcc = acc <> fmap sectionResponseToSection (results res)
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    getSection :: SectionId -> TodoistIO Section
    getSection SectionId {..} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["sections", _id] Nothing Nothing
        resp <- liftIO $ apiGet (Proxy @SectionResponse) config apiRequest
        case resp of
            Right res -> pure $ sectionResponseToSection res
            Left err -> lift $ except (Left err)

    addSection :: SectionCreate -> TodoistIO SectionId
    addSection section = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @SectionCreate ["sections"] Nothing Nothing
        resp <- liftIO $ apiPost (Just section) (JsonResponse (Proxy @SectionId)) config apiRequest
        case resp of
            Right res -> pure res
            Left err -> lift $ except (Left err)

    updateSection :: SectionUpdate -> SectionId -> TodoistIO Section
    updateSection sectionUpdate SectionId {..} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @SectionUpdate ["sections", _id] Nothing Nothing
        resp <-
            liftIO $ apiPost (Just sectionUpdate) (JsonResponse (Proxy @SectionResponse)) config apiRequest
        case resp of
            Right res -> pure $ sectionResponseToSection res
            Left err -> lift $ except (Left err)

    deleteSection :: SectionId -> TodoistIO ()
    deleteSection SectionId {..} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["sections", _id] Nothing Nothing
        resp <- liftIO $ apiDelete config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    getSectionsPaginated :: SectionParam -> TodoistIO ([Section], Maybe Text)
    getSectionsPaginated sectionParams = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["sections"] (Just $ toQueryParam sectionParams) Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn SectionResponse)) config apiRequest
        case resp of
            Right res -> pure (fmap sectionResponseToSection (results res), fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

-- ============================================================================
-- TodoistLabelM Instance
-- ============================================================================

instance TodoistLabelM TodoistIO where
    getLabels :: LabelParam -> TodoistIO [Label]
    getLabels _ = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [Label] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Label]
            loop cursorVal acc = do
                let params = LabelParam {cursor = cursorVal, limit = Nothing}
                    apiRequest = mkTodoistRequest @Void ["labels"] (Just $ toQueryParam params) Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn LabelResponse)) config apiRequest
                case resp of
                    Right res -> do
                        let newAcc = acc <> fmap labelResponseToLabel (results res)
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    getLabel :: LabelId -> TodoistIO Label
    getLabel LabelId {..} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["labels", getLabelId] Nothing Nothing
        resp <- liftIO $ apiGet (Proxy @LabelResponse) config apiRequest
        case resp of
            Right res -> pure $ labelResponseToLabel res
            Left err -> lift $ except (Left err)

    addLabel :: LabelCreate -> TodoistIO LabelId
    addLabel label = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @LabelCreate ["labels"] Nothing Nothing
        resp <- liftIO $ apiPost (Just label) (JsonResponse (Proxy @LabelId)) config apiRequest
        case resp of
            Right res -> pure res
            Left err -> lift $ except (Left err)

    updateLabel :: LabelUpdate -> LabelId -> TodoistIO Label
    updateLabel labelUpdate LabelId {..} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @LabelUpdate ["labels", getLabelId] Nothing Nothing
        resp <-
            liftIO $ apiPost (Just labelUpdate) (JsonResponse (Proxy @LabelResponse)) config apiRequest
        case resp of
            Right res -> pure $ labelResponseToLabel res
            Left err -> lift $ except (Left err)

    deleteLabel :: LabelId -> TodoistIO ()
    deleteLabel LabelId {..} = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["labels", getLabelId] Nothing Nothing
        resp <- liftIO $ apiDelete config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    getLabelsPaginated :: LabelParam -> TodoistIO ([Label], Maybe Text)
    getLabelsPaginated labelParams = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @Void ["labels"] (Just $ toQueryParam labelParams) Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn LabelResponse)) config apiRequest
        case resp of
            Right res -> pure (fmap labelResponseToLabel (results res), fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

    getSharedLabels :: SharedLabelParam -> TodoistIO [Text]
    getSharedLabels (SharedLabelParam initialOmit _ _) = TodoistIO $ do
        config <- ask
        let loop :: Maybe Text -> [Text] -> ReaderT TodoistConfig (ExceptT TodoistError IO) [Text]
            loop cursorVal acc = do
                let params = SharedLabelParam {omit_personal = initialOmit, cursor = cursorVal, limit = Nothing}
                    apiRequest =
                        mkTodoistRequest @Void
                            ["labels", "shared"]
                            (Just $ toQueryParam (params :: SharedLabelParam))
                            Nothing
                resp <- liftIO $ apiGet (Proxy @(TodoistReturn Text)) config apiRequest
                case resp of
                    Right res -> do
                        let newAcc = acc <> results res
                        case next_cursor res of
                            Nothing -> pure newAcc
                            Just c -> loop (Just $ T.pack c) newAcc
                    Left err -> lift $ except (Left err)
        loop Nothing []

    getSharedLabelsPaginated :: SharedLabelParam -> TodoistIO ([Text], Maybe Text)
    getSharedLabelsPaginated sharedParams = TodoistIO $ do
        config <- ask
        let apiRequest =
                mkTodoistRequest @Void
                    ["labels", "shared"]
                    (Just $ toQueryParam (sharedParams :: SharedLabelParam))
                    Nothing
        resp <- liftIO $ apiGet (Proxy @(TodoistReturn Text)) config apiRequest
        case resp of
            Right res -> pure (results res, fmap T.pack (next_cursor res))
            Left err -> lift $ except (Left err)

    removeSharedLabels :: SharedLabelRemove -> TodoistIO ()
    removeSharedLabels removeReq = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @SharedLabelRemove ["labels", "shared", "remove"] Nothing Nothing
        resp <- liftIO $ apiPost (Just removeReq) IgnoreResponse config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)

    renameSharedLabels :: SharedLabelRename -> TodoistIO ()
    renameSharedLabels renameReq = TodoistIO $ do
        config <- ask
        let apiRequest = mkTodoistRequest @SharedLabelRename ["labels", "shared", "rename"] Nothing Nothing
        resp <- liftIO $ apiPost (Just renameReq) IgnoreResponse config apiRequest
        case resp of
            Right _ -> pure ()
            Left err -> lift $ except (Left err)