packages feed

todoist-sdk-0.1.2.1: src/Web/Todoist/Domain/Task.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}

{- |
Module      : Web.Todoist.Domain.Task
Description : Task API types and operations for Todoist REST API
Copyright   : (c) 2025 Sam S. Almahri
License     : MIT
Maintainer  : sam.salmahri@gmail.com

This module provides types and operations for working with Todoist tasks.
Tasks are the core items in Todoist, supporting due dates, priorities, labels,
assignees, and hierarchical sub-tasks.

= Usage Example

@
import Web.Todoist.Domain.Task
import Web.Todoist.Util.Builder
import Web.Todoist.Runner

main :: IO ()
main = do
    let config = newTodoistConfig "your-api-token"

    -- Create a new task
    let task = runBuilder (newTaskBuilder "Buy groceries")
               (withDescription "Milk, eggs, bread" <> withPriority 2)
    taskId <- todoist config (createTask task)

    -- Get all tasks with builder pattern
    let params = runBuilder taskParamBuilder (withProjectId "project-123" <> withLimit 50)
    tasks <- todoist config (getTasks params)

    -- Complete a task
    todoist config (closeTask taskId)
@

For more details, see: <https://developer.todoist.com/rest/v2/#tasks>
-}
module Web.Todoist.Domain.Task
    ( TodoistTaskM (..)
    , TaskParam (..)
    , Task (..)
    , Due (..)
    , Deadline (..)
    , Duration (..)
    , DurationUnit (..)
    , NewTask (..)
    , MoveTask
    , AddTaskQuick
    , addTaskQuickText
    , CompletedTasksQueryParamAPI (..)
    , TaskCompletedItem (..)
    , TaskFilter (..)
    , CompletedTasksQueryParam (..)
    , TaskCreate
    , TaskUpdate
    , newTaskBuilder
    , moveTaskBuilder
    , updateTaskBuilder
    , taskParamBuilder
    , filterTaskBuilder
    , completedTasksQueryParamBuilder
      -- * Lenses
      -- ** Duration
    , amount
    , unit
      -- ** Deadline
    , deadlineDate
    , deadlineLang
      -- ** Due
    , dueDate
    , dueString
    , dueLang
    , dueIsRecurring
    , dueTimezone
      -- ** Task
    , taskId
    , taskContent
    , taskDescription
    , taskProjectId
    , taskSectionId
    , taskParentId
    , taskLabels
    , taskPriority
    , taskDue
    , taskDeadline
    , taskDuration
    , taskIsCollapsed
    , taskOrder
    , taskAssigneeId
    , taskAssignerId
    , taskCompletedAt
    , taskCreatorId
    , taskCreatedAt
    , taskUpdatedAt
      -- ** NewTask
    , newTaskUserId
    , newTaskId
    , newTaskProjectId
    , newTaskSectionId
    , newTaskParentId
    , newTaskAddedByUid
    , newTaskAssignedByUid
    , newTaskResponsibleUid
    , newTaskLabels
    , newTaskChecked
    , newTaskIsDeleted
    , newTaskAddedAt
    , newTaskCompletedAt
    , newTaskUpdatedAt
    , newTaskPriority
    , newTaskChildOrder
    , newTaskContent
    , newTaskDescription
    , newTaskNoteCount
    , newTaskDayOrder
    , newTaskIsCollapsed
      -- ** MoveTask
    , moveTaskProjectId
    , moveTaskSectionId
    , moveTaskParentId
      -- ** AddTaskQuick
    , addTaskQuickTextLens
    , addTaskQuickNote
    , addTaskQuickReminder
    , addTaskQuickAutoReminder
    , addTaskQuickMeta
    ) where

import Web.Todoist.Internal.Types (Params)
import Web.Todoist.Util.Builder
    ( HasAssigneeId (..)
    , HasContent (..)
    , HasCursor (..)
    , HasDeadlineDate (..)
    , HasDescription (..)
    , HasDueDate (..)
    , HasDueDatetime (..)
    , HasDueLang (..)
    , HasDueString (..)
    , HasDuration (..)
    , HasDurationUnit (..)
    , HasFilterLang (..)
    , HasFilterQuery (..)
    , HasLabels (..)
    , HasLang (..)
    , HasLimit (..)
    , HasOrder (..)
    , HasParentId (..)
    , HasPriority (..)
    , HasProjectId (..)
    , HasSectionId (..)
    , HasTaskIds (..)
    , Initial
    , seed
    )
import Web.Todoist.Util.QueryParam (QueryParam (..))

import Control.Monad (Monad)
import Data.Aeson
    ( FromJSON (parseJSON)
    , Options (..)
    , ToJSON (toJSON)
    , Value
    , defaultOptions
    , genericParseJSON
    , genericToJSON
    , omitNothingFields
    )
import Data.Aeson.Types (Parser)
import Data.Bool (Bool (..))
import Data.Eq (Eq)
import Data.Int (Int)
import qualified Data.List as L
import Data.Maybe (Maybe (..), maybe)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text
import GHC.Generics (Generic)
import Lens.Micro (to)
import Text.Show (Show)
import Web.Todoist.Lens (Getter)
import Web.Todoist.Domain.Section (SectionId (..))
import Web.Todoist.Domain.Types
    ( Content (..)
    , Description (..)
    , IsCollapsed (..)
    , Order (..)
    , ParentId (..)
    , ProjectId (..)
    , TaskId (..)
    , Uid
    )

-- | Duration unit for tasks
data DurationUnit = Minute | Day
    deriving (Show, Eq, Generic)

instance FromJSON DurationUnit where
    parseJSON :: Value -> Parser DurationUnit
    parseJSON = genericParseJSON defaultOptions

instance ToJSON DurationUnit where
    toJSON :: DurationUnit -> Value
    toJSON Minute = "minute"
    toJSON Day = "day"

-- | Duration information for a task
data Duration = Duration
    { _amount :: Int
    , _unit :: DurationUnit
    }
    deriving (Show, Generic)

instance FromJSON Duration where
    parseJSON :: Value -> Parser Duration
    parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}

instance ToJSON Duration where
    toJSON :: Duration -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}

-- | Lenses for Duration
amount :: Getter Duration Int
amount = to _amount
{-# INLINE amount #-}

unit :: Getter Duration DurationUnit
unit = to _unit
{-# INLINE unit #-}

-- | Deadline information for a task
data Deadline = Deadline
    { _date :: Text
    , _lang :: Text
    }
    deriving (Show, Generic)

instance FromJSON Deadline where
    parseJSON :: Value -> Parser Deadline
    parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}

instance ToJSON Deadline where
    toJSON :: Deadline -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}

-- | Lenses for Deadline
deadlineDate :: Getter Deadline Text
deadlineDate = to (\(Deadline {_date = x}) -> x)
{-# INLINE deadlineDate #-}

deadlineLang :: Getter Deadline Text
deadlineLang = to (\(Deadline {_lang = x}) -> x)
{-# INLINE deadlineLang #-}

-- | Due date information for a task
data Due = Due
    { _date :: Text
    , _string :: Text
    , _lang :: Text
    , _is_recurring :: Bool
    , _timezone :: Maybe Text
    }
    deriving (Show, Generic)

instance FromJSON Due where
    parseJSON :: Value -> Parser Due
    parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}

instance ToJSON Due where
    toJSON :: Due -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}

-- | Lenses for Due
dueDate :: Getter Due Text
dueDate = to (\(Due {_date = x}) -> x)
{-# INLINE dueDate #-}

dueString :: Getter Due Text
dueString = to (\(Due {_string = x}) -> x)
{-# INLINE dueString #-}

dueLang :: Getter Due Text
dueLang = to (\(Due {_lang = x}) -> x)
{-# INLINE dueLang #-}

dueIsRecurring :: Getter Due Bool
dueIsRecurring = to (\(Due {_is_recurring = x}) -> x)
{-# INLINE dueIsRecurring #-}

dueTimezone :: Getter Due (Maybe Text)
dueTimezone = to (\(Due {_timezone = x}) -> x)
{-# INLINE dueTimezone #-}

{- | Task domain type representing a Todoist task

Contains all task metadata including content, project/section assignment,
due dates, priority, labels, and completion status. Tasks can have sub-tasks
using parent_id and support rich due date information with recurring patterns.
-}
data Task = Task
    { _id :: TaskId
    , _content :: Content
    , _description :: Description
    , _project_id :: ProjectId
    , _section_id :: Maybe SectionId
    , _parent_id :: Maybe ParentId
    , _labels :: [Text]
    , _priority :: Int
    , _due :: Maybe Due
    , _deadline :: Maybe Deadline
    , _duration :: Maybe Duration
    , _is_collapsed :: IsCollapsed
    , _order :: Order
    , _assignee_id :: Maybe Uid
    , _assigner_id :: Maybe Uid
    , _completed_at :: Maybe Text
    , _creator_id :: Uid
    , _created_at :: Text
    , _updated_at :: Text
    }
    deriving (Show, Generic)

instance FromJSON Task where
    parseJSON :: Value -> Parser Task
    parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}

instance ToJSON Task where
    toJSON :: Task -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}

-- | Lenses for Task
taskId :: Getter Task TaskId
taskId = to (\(Task {_id = x}) -> x)
{-# INLINE taskId #-}

taskContent :: Getter Task Content
taskContent = to (\(Task {_content = x}) -> x)
{-# INLINE taskContent #-}

taskDescription :: Getter Task Description
taskDescription = to (\(Task {_description = x}) -> x)
{-# INLINE taskDescription #-}

taskProjectId :: Getter Task ProjectId
taskProjectId = to (\(Task {_project_id = x}) -> x)
{-# INLINE taskProjectId #-}

taskSectionId :: Getter Task (Maybe SectionId)
taskSectionId = to (\(Task {_section_id = x}) -> x)
{-# INLINE taskSectionId #-}

taskParentId :: Getter Task (Maybe ParentId)
taskParentId = to (\(Task {_parent_id = x}) -> x)
{-# INLINE taskParentId #-}

taskLabels :: Getter Task [Text]
taskLabels = to (\(Task {_labels = x}) -> x)
{-# INLINE taskLabels #-}

taskPriority :: Getter Task Int
taskPriority = to (\(Task {_priority = x}) -> x)
{-# INLINE taskPriority #-}

taskDue :: Getter Task (Maybe Due)
taskDue = to (\(Task {_due = x}) -> x)
{-# INLINE taskDue #-}

taskDeadline :: Getter Task (Maybe Deadline)
taskDeadline = to (\(Task {_deadline = x}) -> x)
{-# INLINE taskDeadline #-}

taskDuration :: Getter Task (Maybe Duration)
taskDuration = to (\(Task {_duration = x}) -> x)
{-# INLINE taskDuration #-}

taskIsCollapsed :: Getter Task IsCollapsed
taskIsCollapsed = to (\(Task {_is_collapsed = x}) -> x)
{-# INLINE taskIsCollapsed #-}

taskOrder :: Getter Task Order
taskOrder = to (\(Task {_order = x}) -> x)
{-# INLINE taskOrder #-}

taskAssigneeId :: Getter Task (Maybe Uid)
taskAssigneeId = to (\(Task {_assignee_id = x}) -> x)
{-# INLINE taskAssigneeId #-}

taskAssignerId :: Getter Task (Maybe Uid)
taskAssignerId = to (\(Task {_assigner_id = x}) -> x)
{-# INLINE taskAssignerId #-}

taskCompletedAt :: Getter Task (Maybe Text)
taskCompletedAt = to (\(Task {_completed_at = x}) -> x)
{-# INLINE taskCompletedAt #-}

taskCreatorId :: Getter Task Uid
taskCreatorId = to (\(Task {_creator_id = x}) -> x)
{-# INLINE taskCreatorId #-}

taskCreatedAt :: Getter Task Text
taskCreatedAt = to (\(Task {_created_at = x}) -> x)
{-# INLINE taskCreatedAt #-}

taskUpdatedAt :: Getter Task Text
taskUpdatedAt = to (\(Task {_updated_at = x}) -> x)
{-# INLINE taskUpdatedAt #-}

data NewTask = NewTask
    { _user_id :: Text
    , _id :: TaskId
    , _project_id :: ProjectId
    , _section_id :: Maybe SectionId
    , _parent_id :: Maybe ParentId
    , _added_by_uid :: Maybe Uid
    , _assigned_by_uid :: Maybe Uid
    , _responsible_uid :: Maybe Uid
    , _labels :: [Text]
    , _checked :: Bool
    , _is_deleted :: Bool
    , _added_at :: Maybe Text
    , _completed_at :: Maybe Text
    , _updated_at :: Maybe Text
    , _priority :: Int
    , _child_order :: Order
    , _content :: Content
    , _description :: Description
    , _note_count :: Int
    , _day_order :: Order
    , _is_collapsed :: IsCollapsed
    }
    deriving (Show, Generic)

instance FromJSON NewTask where
    parseJSON :: Value -> Parser NewTask
    parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}

instance ToJSON NewTask where
    toJSON :: NewTask -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}

-- | Lenses for NewTask
newTaskUserId :: Getter NewTask Text
newTaskUserId = to (\(NewTask {_user_id = x}) -> x)
{-# INLINE newTaskUserId #-}

newTaskId :: Getter NewTask TaskId
newTaskId = to (\(NewTask {_id = x}) -> x)
{-# INLINE newTaskId #-}

newTaskProjectId :: Getter NewTask ProjectId
newTaskProjectId = to (\(NewTask {_project_id = x}) -> x)
{-# INLINE newTaskProjectId #-}

newTaskSectionId :: Getter NewTask (Maybe SectionId)
newTaskSectionId = to (\(NewTask {_section_id = x}) -> x)
{-# INLINE newTaskSectionId #-}

newTaskParentId :: Getter NewTask (Maybe ParentId)
newTaskParentId = to (\(NewTask {_parent_id = x}) -> x)
{-# INLINE newTaskParentId #-}

newTaskAddedByUid :: Getter NewTask (Maybe Uid)
newTaskAddedByUid = to (\(NewTask {_added_by_uid = x}) -> x)
{-# INLINE newTaskAddedByUid #-}

newTaskAssignedByUid :: Getter NewTask (Maybe Uid)
newTaskAssignedByUid = to (\(NewTask {_assigned_by_uid = x}) -> x)
{-# INLINE newTaskAssignedByUid #-}

newTaskResponsibleUid :: Getter NewTask (Maybe Uid)
newTaskResponsibleUid = to (\(NewTask {_responsible_uid = x}) -> x)
{-# INLINE newTaskResponsibleUid #-}

newTaskLabels :: Getter NewTask [Text]
newTaskLabels = to (\(NewTask {_labels = x}) -> x)
{-# INLINE newTaskLabels #-}

newTaskChecked :: Getter NewTask Bool
newTaskChecked = to (\(NewTask {_checked = x}) -> x)
{-# INLINE newTaskChecked #-}

newTaskIsDeleted :: Getter NewTask Bool
newTaskIsDeleted = to (\(NewTask {_is_deleted = x}) -> x)
{-# INLINE newTaskIsDeleted #-}

newTaskAddedAt :: Getter NewTask (Maybe Text)
newTaskAddedAt = to (\(NewTask {_added_at = x}) -> x)
{-# INLINE newTaskAddedAt #-}

newTaskCompletedAt :: Getter NewTask (Maybe Text)
newTaskCompletedAt = to (\(NewTask {_completed_at = x}) -> x)
{-# INLINE newTaskCompletedAt #-}

newTaskUpdatedAt :: Getter NewTask (Maybe Text)
newTaskUpdatedAt = to (\(NewTask {_updated_at = x}) -> x)
{-# INLINE newTaskUpdatedAt #-}

newTaskPriority :: Getter NewTask Int
newTaskPriority = to (\(NewTask {_priority = x}) -> x)
{-# INLINE newTaskPriority #-}

newTaskChildOrder :: Getter NewTask Order
newTaskChildOrder = to (\(NewTask {_child_order = x}) -> x)
{-# INLINE newTaskChildOrder #-}

newTaskContent :: Getter NewTask Content
newTaskContent = to (\(NewTask {_content = x}) -> x)
{-# INLINE newTaskContent #-}

newTaskDescription :: Getter NewTask Description
newTaskDescription = to (\(NewTask {_description = x}) -> x)
{-# INLINE newTaskDescription #-}

newTaskNoteCount :: Getter NewTask Int
newTaskNoteCount = to (\(NewTask {_note_count = x}) -> x)
{-# INLINE newTaskNoteCount #-}

newTaskDayOrder :: Getter NewTask Order
newTaskDayOrder = to (\(NewTask {_day_order = x}) -> x)
{-# INLINE newTaskDayOrder #-}

newTaskIsCollapsed :: Getter NewTask IsCollapsed
newTaskIsCollapsed = to (\(NewTask {_is_collapsed = x}) -> x)
{-# INLINE newTaskIsCollapsed #-}

data MoveTask = MoveTask
    { _project_id :: Maybe ProjectId
    , _section_id :: Maybe SectionId
    , _parent_id :: Maybe ParentId
    }
    deriving (Show, Generic)

instance ToJSON MoveTask where
    toJSON :: MoveTask -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}

instance FromJSON MoveTask where
    parseJSON :: Value -> Parser MoveTask
    parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}

-- | Lenses for MoveTask
moveTaskProjectId :: Getter MoveTask (Maybe ProjectId)
moveTaskProjectId = to (\(MoveTask {_project_id = x}) -> x)
{-# INLINE moveTaskProjectId #-}

moveTaskSectionId :: Getter MoveTask (Maybe SectionId)
moveTaskSectionId = to (\(MoveTask {_section_id = x}) -> x)
{-# INLINE moveTaskSectionId #-}

moveTaskParentId :: Getter MoveTask (Maybe ParentId)
moveTaskParentId = to (\(MoveTask {_parent_id = x}) -> x)
{-# INLINE moveTaskParentId #-}

-- | Create empty MoveTask for use with builder pattern
moveTaskBuilder :: Initial MoveTask
moveTaskBuilder =
    seed
        MoveTask
            { _project_id = Nothing
            , _section_id = Nothing
            , _parent_id = Nothing
            }

instance HasProjectId MoveTask where
    hasProjectId :: Text -> MoveTask -> MoveTask
    hasProjectId projId MoveTask {..} = MoveTask {_project_id = Just (ProjectId projId), ..}

instance HasSectionId MoveTask where
    hasSectionId :: Text -> MoveTask -> MoveTask
    hasSectionId secId MoveTask {..} = MoveTask {_section_id = Just (SectionId {_id = secId}), ..}

instance HasParentId MoveTask where
    hasParentId :: Text -> MoveTask -> MoveTask
    hasParentId parId MoveTask {..} = MoveTask {_parent_id = Just (ParentId parId), ..}

data AddTaskQuick = AddTaskQuick
    { _text :: Text
    , _note :: Maybe Text
    , _reminder :: Maybe Text
    , _auto_reminder :: Bool
    , _meta :: Bool
    }
    deriving (Show, Generic, FromJSON, ToJSON)

-- | Lenses for AddTaskQuick
addTaskQuickTextLens :: Getter AddTaskQuick Text
addTaskQuickTextLens = to (\(AddTaskQuick {_text = x}) -> x)
{-# INLINE addTaskQuickTextLens #-}

addTaskQuickNote :: Getter AddTaskQuick (Maybe Text)
addTaskQuickNote = to (\(AddTaskQuick {_note = x}) -> x)
{-# INLINE addTaskQuickNote #-}

addTaskQuickReminder :: Getter AddTaskQuick (Maybe Text)
addTaskQuickReminder = to (\(AddTaskQuick {_reminder = x}) -> x)
{-# INLINE addTaskQuickReminder #-}

addTaskQuickAutoReminder :: Getter AddTaskQuick Bool
addTaskQuickAutoReminder = to (\(AddTaskQuick {_auto_reminder = x}) -> x)
{-# INLINE addTaskQuickAutoReminder #-}

addTaskQuickMeta :: Getter AddTaskQuick Bool
addTaskQuickMeta = to (\(AddTaskQuick {_meta = x}) -> x)
{-# INLINE addTaskQuickMeta #-}

addTaskQuickText :: Text -> AddTaskQuick
addTaskQuickText text =
    AddTaskQuick
        { _text = text
        , _note = Nothing
        , _reminder = Nothing
        , _auto_reminder = False
        , _meta = False
        }

{- | Request body for creating a new task

Only content is required. Use 'newTaskBuilder' with the builder pattern for
ergonomic construction of tasks with optional fields.
-}
data TaskCreate = TaskCreate
    { _content :: Content
    , _description :: Maybe Description
    , _project_id :: Maybe ProjectId
    , _section_id :: Maybe SectionId
    , _parent_id :: Maybe ParentId
    , _order :: Maybe Order
    , _labels :: Maybe [Text]
    , _priority :: Maybe Int
    , _assignee_id :: Maybe Int
    , _due_string :: Maybe Text
    , _due_date :: Maybe Text
    , _due_datetime :: Maybe Text
    , _due_lang :: Maybe Text
    , _duration :: Maybe Int
    , _duration_unit :: Maybe Text
    , _deadline_date :: Maybe Text
    }
    deriving (Show, Generic)

instance ToJSON TaskCreate where
    toJSON :: TaskCreate -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1, omitNothingFields = True}

-- | Create new TaskCreate with required content parameter
newTaskBuilder :: Text -> Initial TaskCreate
newTaskBuilder content =
    seed
        TaskCreate
            { _content = Content content
            , _description = Nothing
            , _project_id = Nothing
            , _section_id = Nothing
            , _parent_id = Nothing
            , _order = Nothing
            , _labels = Nothing
            , _priority = Nothing
            , _assignee_id = Nothing
            , _due_string = Nothing
            , _due_date = Nothing
            , _due_datetime = Nothing
            , _due_lang = Nothing
            , _duration = Nothing
            , _duration_unit = Nothing
            , _deadline_date = Nothing
            }

instance HasDescription TaskCreate where
    hasDescription :: Text -> TaskCreate -> TaskCreate
    hasDescription desc TaskCreate {..} = TaskCreate {_description = Just (Description desc), ..}

instance HasContent TaskCreate where
    hasContent :: Text -> TaskCreate -> TaskCreate
    hasContent content TaskCreate {..} = TaskCreate {_content = Content content, ..}

instance HasSectionId TaskCreate where
    hasSectionId :: Text -> TaskCreate -> TaskCreate
    hasSectionId sid TaskCreate {..} = TaskCreate {_section_id = Just (SectionId {_id = sid}), ..}

instance HasParentId TaskCreate where
    hasParentId :: Text -> TaskCreate -> TaskCreate
    hasParentId pid TaskCreate {..} = TaskCreate {_parent_id = Just (ParentId pid), ..}

instance HasOrder TaskCreate where
    hasOrder :: Int -> TaskCreate -> TaskCreate
    hasOrder order TaskCreate {..} = TaskCreate {_order = Just (Order order), ..}

instance HasLabels TaskCreate where
    hasLabels :: [Text] -> TaskCreate -> TaskCreate
    hasLabels labels TaskCreate {..} = TaskCreate {_labels = Just labels, ..}

instance HasPriority TaskCreate where
    hasPriority :: Int -> TaskCreate -> TaskCreate
    hasPriority priority TaskCreate {..} = TaskCreate {_priority = Just priority, ..}

instance HasAssigneeId TaskCreate where
    hasAssigneeId :: Int -> TaskCreate -> TaskCreate
    hasAssigneeId aid TaskCreate {..} = TaskCreate {_assignee_id = Just aid, ..}

instance HasDueString TaskCreate where
    hasDueString :: Text -> TaskCreate -> TaskCreate
    hasDueString dueStr TaskCreate {..} = TaskCreate {_due_string = Just dueStr, ..}

instance HasDueDate TaskCreate where
    hasDueDate :: Text -> TaskCreate -> TaskCreate
    hasDueDate dueDate TaskCreate {..} = TaskCreate {_due_date = Just dueDate, ..}

instance HasDueDatetime TaskCreate where
    hasDueDatetime :: Text -> TaskCreate -> TaskCreate
    hasDueDatetime dueDatetime TaskCreate {..} = TaskCreate {_due_datetime = Just dueDatetime, ..}

instance HasDueLang TaskCreate where
    hasDueLang :: Text -> TaskCreate -> TaskCreate
    hasDueLang dueLang TaskCreate {..} = TaskCreate {_due_lang = Just dueLang, ..}

instance HasDuration TaskCreate where
    hasDuration :: Int -> TaskCreate -> TaskCreate
    hasDuration duration TaskCreate {..} = TaskCreate {_duration = Just duration, ..}

instance HasDurationUnit TaskCreate where
    hasDurationUnit :: Text -> TaskCreate -> TaskCreate
    hasDurationUnit durationUnit TaskCreate {..} = TaskCreate {_duration_unit = Just durationUnit, ..}

instance HasDeadlineDate TaskCreate where
    hasDeadlineDate :: Text -> TaskCreate -> TaskCreate
    hasDeadlineDate deadlineDate TaskCreate {..} = TaskCreate {_deadline_date = Just deadlineDate, ..}

instance HasProjectId TaskCreate where
    hasProjectId :: Text -> TaskCreate -> TaskCreate
    hasProjectId projId TaskCreate {..} = TaskCreate {_project_id = Just (ProjectId projId), ..}

{- | Request body for updating an existing task (partial updates)

All fields are optional (using Maybe). Only provided fields will be updated.
Use 'updateTaskBuilder' with the builder pattern for updates.
-}
data TaskUpdate = TaskUpdate
    { _content :: Maybe Content
    , _description :: Maybe Description
    , _labels :: Maybe [Text]
    , _priority :: Maybe Int
    , _due_string :: Maybe Text
    , _due_date :: Maybe Text
    , _due_datetime :: Maybe Text
    , _due_lang :: Maybe Text
    , _assignee_id :: Maybe Int
    , _duration :: Maybe Int
    , _duration_unit :: Maybe Text
    , _deadline_date :: Maybe Text
    , _deadline_lang :: Maybe Text
    }
    deriving (Show, Generic)

updateTaskBuilder :: Initial TaskUpdate
updateTaskBuilder =
    seed
        TaskUpdate
            { _content = Nothing
            , _description = Nothing
            , _labels = Nothing
            , _priority = Nothing
            , _due_string = Nothing
            , _due_date = Nothing
            , _due_datetime = Nothing
            , _due_lang = Nothing
            , _assignee_id = Nothing
            , _duration = Nothing
            , _duration_unit = Nothing
            , _deadline_date = Nothing
            , _deadline_lang = Nothing
            }

instance ToJSON TaskUpdate where
    toJSON :: TaskUpdate -> Value
    toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1, omitNothingFields = True}

instance HasDescription TaskUpdate where
    hasDescription :: Text -> TaskUpdate -> TaskUpdate
    hasDescription desc TaskUpdate {..} = TaskUpdate {_description = Just (Description desc), ..}

instance HasContent TaskUpdate where
    hasContent :: Text -> TaskUpdate -> TaskUpdate
    hasContent content TaskUpdate {..} = TaskUpdate {_content = Just (Content content), ..}

instance HasLabels TaskUpdate where
    hasLabels :: [Text] -> TaskUpdate -> TaskUpdate
    hasLabels labels TaskUpdate {..} = TaskUpdate {_labels = Just labels, ..}

instance HasPriority TaskUpdate where
    hasPriority :: Int -> TaskUpdate -> TaskUpdate
    hasPriority priority TaskUpdate {..} = TaskUpdate {_priority = Just priority, ..}

instance HasDueString TaskUpdate where
    hasDueString :: Text -> TaskUpdate -> TaskUpdate
    hasDueString dueStr TaskUpdate {..} = TaskUpdate {_due_string = Just dueStr, ..}

instance HasDueDate TaskUpdate where
    hasDueDate :: Text -> TaskUpdate -> TaskUpdate
    hasDueDate dueDate TaskUpdate {..} = TaskUpdate {_due_date = Just dueDate, ..}

instance HasDueDatetime TaskUpdate where
    hasDueDatetime :: Text -> TaskUpdate -> TaskUpdate
    hasDueDatetime dueDatetime TaskUpdate {..} = TaskUpdate {_due_datetime = Just dueDatetime, ..}

instance HasDueLang TaskUpdate where
    hasDueLang :: Text -> TaskUpdate -> TaskUpdate
    hasDueLang dueLang TaskUpdate {..} = TaskUpdate {_due_lang = Just dueLang, ..}

instance HasAssigneeId TaskUpdate where
    hasAssigneeId :: Int -> TaskUpdate -> TaskUpdate
    hasAssigneeId aid TaskUpdate {..} = TaskUpdate {_assignee_id = Just aid, ..}

instance HasDuration TaskUpdate where
    hasDuration :: Int -> TaskUpdate -> TaskUpdate
    hasDuration duration TaskUpdate {..} = TaskUpdate {_duration = Just duration, ..}

instance HasDurationUnit TaskUpdate where
    hasDurationUnit :: Text -> TaskUpdate -> TaskUpdate
    hasDurationUnit durationUnit TaskUpdate {..} = TaskUpdate {_duration_unit = Just durationUnit, ..}

instance HasDeadlineDate TaskUpdate where
    hasDeadlineDate :: Text -> TaskUpdate -> TaskUpdate
    hasDeadlineDate deadlineDate TaskUpdate {..} = TaskUpdate {_deadline_date = Just deadlineDate, ..}

{- | Internal type for parsing completed tasks API response
The API returns full task objects, not just IDs
-}
newtype CompletedTasksQueryParamAPI = CompletedTasksQueryParamAPI
    { items :: [TaskCompletedItem]
    }
    deriving (Show, Generic, FromJSON)

{- | Minimal task representation for completed tasks response
Only contains the id field we need from the completed tasks API
-}
newtype TaskCompletedItem = TaskCompletedItem
    { id :: Text
    }
    deriving (Show, Generic, FromJSON)

class (Monad m) => TodoistTaskM m where
    -- | Get tasks (automatically fetches all pages)
    getTasks :: TaskParam -> m [Task]

    -- | Get a single task by ID
    getTask :: TaskId -> m Task

    -- | Create a new task
    createTask :: TaskCreate -> m NewTask -- todo: should return Task; TaskCreate should be named NewTask

    -- | Update an existing task with partial changes
    updateTask :: TaskUpdate -> TaskId -> m NewTask -- todo: should return Task

    -- | Mark a task as completed
    closeTask :: TaskId -> m ()

    -- | Reopen a previously completed task
    uncloseTask :: TaskId -> m ()

    -- | Permanently delete a task
    deleteTask :: TaskId -> m ()

    -- | Get tasks by filter (automatically fetches all pages)
    getTasksByFilter :: TaskFilter -> m [TaskId]

    -- | Move a task to a different project or section
    moveTask :: MoveTask -> TaskId -> m TaskId

    addTaskQuick :: AddTaskQuick -> m ()

    getCompletedTasksByDueDate :: CompletedTasksQueryParam -> m [TaskId]

    getCompletedTasksByCompletionDate :: CompletedTasksQueryParam -> m [TaskId]

    {- | Get tasks with manual pagination control
    Returns a tuple of (results, next_cursor) for the requested page
    -}
    getTasksPaginated :: TaskParam -> m ([TaskId], Maybe Text)

    {- | Get tasks by filter with manual pagination control
    Returns a tuple of (results, next_cursor) for the requested page
    Note: TaskFilter already has cursor and limit fields
    -}
    getTasksByFilterPaginated :: TaskFilter -> m ([TaskId], Maybe Text)

    -- | Get all tasks with custom page size (fetches all pages automatically)
    getTasksWithLimit :: Int -> TaskParam -> m [TaskId]

    -- | Get all tasks by filter with custom page size (fetches all pages automatically)
    getTasksByFilterWithLimit :: TaskFilter -> Int -> m [TaskId]

-- | Query parameters for filtering tasks
data TaskParam = TaskParam
    { project_id :: Maybe Text
    , section_id :: Maybe Text
    , parent_id :: Maybe Text
    , task_ids :: [Text]
    , cursor :: Maybe Text
    , limit :: Maybe Int
    }
    deriving (Show)

instance QueryParam TaskParam where
    toQueryParam :: TaskParam -> Params
    toQueryParam TaskParam {..} =
        maybe [] (\projId -> [("project_id", projId)]) project_id
            <> maybe [] (\secId -> [("section_id", secId)]) section_id
            <> maybe [] (\parId -> [("parent_id", parId)]) parent_id
            <> L.map ("task_id",) task_ids
            <> maybe [] (\c -> [("cursor", c)]) cursor
            <> maybe [] (\l -> [("limit", Data.Text.show l)]) limit

-- | Query parameters for filtering tasks by text query
data TaskFilter = TaskFilter
    { query :: Text
    , lang :: Maybe Text
    , cursor :: Maybe Text
    , limit :: Maybe Int
    }
    deriving (Show)

instance QueryParam TaskFilter where
    toQueryParam :: TaskFilter -> Params
    toQueryParam TaskFilter {..} =
        [("query", query)]
            <> maybe [] (\p -> [("lang", p)]) lang
            <> maybe [] (\p -> [("cursor", p)]) cursor
            <> maybe [] (\p -> [("limit", Data.Text.show p)]) limit

-- | Create new TaskFilter with required query parameter
filterTaskBuilder :: Text -> Initial TaskFilter
filterTaskBuilder query =
    seed
        TaskFilter
            { query
            , lang = Nothing
            , cursor = Nothing
            , limit = Nothing
            }

-- | Query parameters for getting completed tasks
data CompletedTasksQueryParam = CompletedTasksQueryParam
    { since :: Text
    , until :: Text
    , workspace_id :: Maybe Text
    , project_id :: Maybe Text
    , section_id :: Maybe Text
    , parent_id :: Maybe Text
    , filter_query :: Maybe Text
    , filter_lang :: Maybe Text
    , cursor :: Maybe Text
    , limit :: Int
    }
    deriving (Show)

instance QueryParam CompletedTasksQueryParam where
    toQueryParam :: CompletedTasksQueryParam -> Params
    toQueryParam CompletedTasksQueryParam {..} =
        [("since", since)]
            <> [("until", until)]
            <> maybe [] (\p -> [("workspace_id", p)]) workspace_id
            <> maybe [] (\p -> [("project_id", p)]) project_id
            <> maybe [] (\p -> [("section_id", p)]) section_id
            <> maybe [] (\p -> [("parent_id", p)]) parent_id
            <> maybe [] (\p -> [("filter_query", p)]) filter_query
            <> maybe [] (\p -> [("filter_lang", p)]) filter_lang
            <> maybe [] (\p -> [("cursor", p)]) cursor
            <> [("limit", Data.Text.show limit)]

-- | Create new CompletedTasksQueryParam with required since/until parameters
completedTasksQueryParamBuilder :: Text -> Text -> Initial CompletedTasksQueryParam
completedTasksQueryParamBuilder since until =
    seed
        CompletedTasksQueryParam
            { since
            , until
            , workspace_id = Nothing
            , project_id = Nothing
            , section_id = Nothing
            , parent_id = Nothing
            , filter_query = Nothing
            , filter_lang = Nothing
            , cursor = Nothing
            , limit = 50
            }

-- | Create empty TaskParam for use with builder pattern
taskParamBuilder :: Initial TaskParam
taskParamBuilder =
    seed
        TaskParam
            { project_id = Nothing
            , section_id = Nothing
            , parent_id = Nothing
            , task_ids = []
            , cursor = Nothing
            , limit = Nothing
            }

-- HasX instances for TaskParam
instance HasProjectId TaskParam where
    hasProjectId :: Text -> TaskParam -> TaskParam
    hasProjectId pid TaskParam {..} = TaskParam {project_id = Just pid, ..}

instance HasSectionId TaskParam where
    hasSectionId :: Text -> TaskParam -> TaskParam
    hasSectionId sid TaskParam {..} = TaskParam {section_id = Just sid, ..}

instance HasParentId TaskParam where
    hasParentId :: Text -> TaskParam -> TaskParam
    hasParentId pid TaskParam {..} = TaskParam {parent_id = Just pid, ..}

instance HasTaskIds TaskParam where
    hasTaskIds :: [Text] -> TaskParam -> TaskParam
    hasTaskIds tids TaskParam {..} = TaskParam {task_ids = tids, ..}

instance HasCursor TaskParam where
    hasCursor :: Text -> TaskParam -> TaskParam
    hasCursor c TaskParam {..} = TaskParam {cursor = Just c, ..}

instance HasLimit TaskParam where
    hasLimit :: Int -> TaskParam -> TaskParam
    hasLimit l TaskParam {..} = TaskParam {limit = Just l, ..}

-- HasX instances for TaskFilter
instance HasLang TaskFilter where
    hasLang :: Text -> TaskFilter -> TaskFilter
    hasLang lng TaskFilter {..} = TaskFilter {lang = Just lng, ..}

instance HasCursor TaskFilter where
    hasCursor :: Text -> TaskFilter -> TaskFilter
    hasCursor c TaskFilter {..} = TaskFilter {cursor = Just c, ..}

instance HasLimit TaskFilter where
    hasLimit :: Int -> TaskFilter -> TaskFilter
    hasLimit l TaskFilter {..} = TaskFilter {limit = Just l, ..}

-- HasX instances for CompletedTasksQueryParam
instance HasProjectId CompletedTasksQueryParam where
    hasProjectId :: Text -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasProjectId pid CompletedTasksQueryParam {..} = CompletedTasksQueryParam {project_id = Just pid, ..}

instance HasSectionId CompletedTasksQueryParam where
    hasSectionId :: Text -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasSectionId sid CompletedTasksQueryParam {..} = CompletedTasksQueryParam {section_id = Just sid, ..}

instance HasParentId CompletedTasksQueryParam where
    hasParentId :: Text -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasParentId pid CompletedTasksQueryParam {..} = CompletedTasksQueryParam {parent_id = Just pid, ..}

instance HasFilterQuery CompletedTasksQueryParam where
    hasFilterQuery :: Text -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasFilterQuery fq CompletedTasksQueryParam {..} = CompletedTasksQueryParam {filter_query = Just fq, ..}

instance HasFilterLang CompletedTasksQueryParam where
    hasFilterLang :: Text -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasFilterLang fl CompletedTasksQueryParam {..} = CompletedTasksQueryParam {filter_lang = Just fl, ..}

instance HasCursor CompletedTasksQueryParam where
    hasCursor :: Text -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasCursor c CompletedTasksQueryParam {..} = CompletedTasksQueryParam {cursor = Just c, ..}

instance HasLimit CompletedTasksQueryParam where
    hasLimit :: Int -> CompletedTasksQueryParam -> CompletedTasksQueryParam
    hasLimit l CompletedTasksQueryParam {..} = CompletedTasksQueryParam {limit = l, ..}