// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

import { getStoragePort, type TaskPriority, type TaskRecord } from './storage.js';
import {
  asInteger,
  asNonEmptyString,
  asStringArray,
  buildAuditEvent,
  createId,
  failure,
  includesText,
  isDryRun,
  matchesTags,
  nowIso,
  success,
  toRecord,
  type ToolContextLike,
  type ToolOutput,
} from './shared.js';
import {
  buildTaskCompletedEvent,
  buildTaskCreatedEvent,
  buildTaskScheduledEvent,
  buildTaskSnoozedEvent,
  emitSidekickEvent,
} from '../sidekick-events.js';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const TOOL_NAMES = {
  CREATE: 'task:create',
  LIST: 'task:list',
  UPDATE: 'task:update',
  CANCEL: 'task:cancel',
  COMPLETE: 'task:complete',
  SCHEDULE: 'task:schedule',
} as const;

const VALID_PRIORITIES: TaskPriority[] = ['P0', 'P1', 'P2', 'P3'];
const DEFAULT_PRIORITY: TaskPriority = 'P2';
const TASK_ID_REQUIRED_MESSAGE = 'id is required.';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function asPriority(value: unknown): TaskPriority {
  return VALID_PRIORITIES.includes(value as TaskPriority)
    ? (value as TaskPriority)
    : DEFAULT_PRIORITY;
}

function resolveTaskUpdatePatch(parsed: Record<string, unknown>): Partial<TaskRecord> {
  const patch: Partial<TaskRecord> = {};
  const title = asNonEmptyString(parsed.title);
  const description =
    typeof parsed.description === 'string'
      ? (asNonEmptyString(parsed.description) ?? undefined)
      : null;
  const dueAt =
    typeof parsed.due_at === 'string' ? (asNonEmptyString(parsed.due_at) ?? undefined) : null;
  const cron =
    typeof parsed.cron === 'string' ? (asNonEmptyString(parsed.cron) ?? undefined) : null;

  if (title) {
    patch.title = title;
  }
  if (description !== null) {
    patch.description = description;
  }
  if (dueAt !== null) {
    patch.due_at = dueAt;
  }
  if (cron !== null) {
    patch.cron = cron;
  }
  if (parsed.priority !== undefined) {
    patch.priority = asPriority(parsed.priority);
  }
  if (parsed.tags !== undefined) {
    patch.tags = asStringArray(parsed.tags);
  }

  return patch;
}

function applyTaskPatch(task: TaskRecord, patch: Partial<TaskRecord>): TaskRecord {
  return {
    ...task,
    ...patch,
    updated_at: nowIso(),
  };
}

function buildCanceledTask(task: TaskRecord): TaskRecord {
  return {
    ...task,
    status: 'canceled',
    canceled_at: nowIso(),
    updated_at: nowIso(),
  };
}

function movedDueDateLater(
  previousDueAt: string | undefined,
  nextDueAt: string | undefined,
): boolean {
  if (!previousDueAt || !nextDueAt) {
    return false;
  }
  return Date.parse(nextDueAt) > Date.parse(previousDueAt);
}

// ---------------------------------------------------------------------------
// task:create
// ---------------------------------------------------------------------------

async function taskCreateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const title = asNonEmptyString(parsed.title);

  if (!title) {
    return failure('INVALID_INPUT', 'title is required.');
  }

  const task: TaskRecord = {
    id: createId('task'),
    title,
    description: asNonEmptyString(parsed.description) ?? undefined,
    priority: asPriority(parsed.priority),
    status: 'pending',
    tags: asStringArray(parsed.tags),
    due_at: asNonEmptyString(parsed.due_at) ?? undefined,
    created_at: nowIso(),
    updated_at: nowIso(),
  };

  if (isDryRun(parsed)) {
    return success({ dry_run: true, task: task as unknown as Record<string, unknown> });
  }

  const storage = getStoragePort();
  await storage.withLock(async () => {
    const tasks = await storage.readStore('tasks');
    tasks.push(task);
    await storage.writeStore('tasks', tasks);
    await storage.appendAudit(
      buildAuditEvent({
        tool: TOOL_NAMES.CREATE,
        op: 'create',
        context,
        ids: [task.id],
      }),
    );
  });

  await emitSidekickEvent(buildTaskCreatedEvent(task));

  return success({ task: task as unknown as Record<string, unknown> });
}

// ---------------------------------------------------------------------------
// task:list
// ---------------------------------------------------------------------------

async function taskListTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const statusFilter = asNonEmptyString(parsed.status);
  const priorityFilter = asNonEmptyString(parsed.priority);
  const tags = asStringArray(parsed.tags);
  const search = asNonEmptyString(parsed.search);
  const dueBefore = asNonEmptyString(parsed.due_before);
  const limit = asInteger(parsed.limit);

  const storage = getStoragePort();
  const tasks = await storage.readStore('tasks');

  const filtered = tasks.filter((task) => {
    if (statusFilter && task.status !== statusFilter) {
      return false;
    }
    if (priorityFilter && task.priority !== priorityFilter) {
      return false;
    }
    if (!matchesTags(tags, task.tags)) {
      return false;
    }
    if (!includesText(`${task.title}\n${task.description ?? ''}`, search)) {
      return false;
    }
    if (dueBefore && task.due_at) {
      if (Date.parse(task.due_at) >= Date.parse(dueBefore)) {
        return false;
      }
    }
    if (dueBefore && !task.due_at) {
      return false;
    }
    return true;
  });

  const sorted = filtered.toSorted((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));

  const items = limit && limit > 0 ? sorted.slice(0, limit) : sorted;

  return success({
    items: items as unknown as Record<string, unknown>,
    count: items.length,
  });
}

// ---------------------------------------------------------------------------
// task:update
// ---------------------------------------------------------------------------

async function taskUpdateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const id = asNonEmptyString(parsed.id);

  if (!id) {
    return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
  }

  const patch = resolveTaskUpdatePatch(parsed);
  if (Object.keys(patch).length === 0) {
    return failure(
      'INVALID_INPUT',
      'task:update requires at least one of title, description, priority, tags, due_at, or cron.',
    );
  }

  const storage = getStoragePort();
  const tasks = await storage.readStore('tasks');
  const task = tasks.find((entry) => entry.id === id);

  if (!task) {
    return failure('NOT_FOUND', `task ${id} was not found.`);
  }
  if (task.status === 'canceled') {
    return failure(
      'INVALID_STATE',
      `task ${id} is canceled and cannot be completed. Reopen it with task:update before completing.`,
    );
  }

  if (isDryRun(parsed)) {
    return success({
      dry_run: true,
      task: applyTaskPatch(task, patch) as unknown as Record<string, unknown>,
    });
  }

  const previousDueAt = task.due_at ?? null;

  await storage.withLock(async () => {
    const latest = await storage.readStore('tasks');
    const target = latest.find((entry) => entry.id === id);
    if (!target) {
      return;
    }
    Object.assign(target, applyTaskPatch(target, patch));
    await storage.writeStore('tasks', latest);
    await storage.appendAudit(
      buildAuditEvent({
        tool: TOOL_NAMES.UPDATE,
        op: 'update',
        context,
        ids: [id],
      }),
    );
  });

  const updated = await storage.readStore('tasks');
  const updatedTask = updated.find((entry) => entry.id === id);
  if (updatedTask && movedDueDateLater(previousDueAt ?? undefined, updatedTask.due_at)) {
    await emitSidekickEvent(
      buildTaskSnoozedEvent({
        task_id: id,
        previous_due_at: previousDueAt,
        due_at: updatedTask.due_at ?? previousDueAt ?? '',
      }),
    );
  }
  return success({ task: updatedTask as unknown as Record<string, unknown> });
}

// ---------------------------------------------------------------------------
// task:complete
// ---------------------------------------------------------------------------

async function taskCompleteTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const id = asNonEmptyString(parsed.id);

  if (!id) {
    return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
  }

  const storage = getStoragePort();
  const tasks = await storage.readStore('tasks');
  const task = tasks.find((t) => t.id === id);

  if (!task) {
    return failure('NOT_FOUND', `task ${id} was not found.`);
  }

  if (isDryRun(parsed)) {
    const preview = { ...task, status: 'done' as const, completed_at: nowIso() };
    return success({
      dry_run: true,
      task: preview as unknown as Record<string, unknown>,
    });
  }

  await storage.withLock(async () => {
    const latest = await storage.readStore('tasks');
    const target = latest.find((t) => t.id === id);
    if (target) {
      target.status = 'done';
      target.completed_at = nowIso();
      target.updated_at = nowIso();
      if (parsed.note) {
        target.note = asNonEmptyString(parsed.note) ?? undefined;
      }
      await storage.writeStore('tasks', latest);
      await storage.appendAudit(
        buildAuditEvent({
          tool: TOOL_NAMES.COMPLETE,
          op: 'update',
          context,
          ids: [id],
        }),
      );
    }
  });

  const updated = await storage.readStore('tasks');
  const completedTask = updated.find((t) => t.id === id);
  if (completedTask) {
    await emitSidekickEvent(buildTaskCompletedEvent(completedTask));
  }

  return success({ task: completedTask as unknown as Record<string, unknown> });
}

// ---------------------------------------------------------------------------
// task:cancel
// ---------------------------------------------------------------------------

async function taskCancelTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const id = asNonEmptyString(parsed.id);

  if (!id) {
    return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
  }

  const storage = getStoragePort();
  const tasks = await storage.readStore('tasks');
  const task = tasks.find((entry) => entry.id === id);

  if (!task) {
    return failure('NOT_FOUND', `task ${id} was not found.`);
  }
  if (task.status === 'done') {
    return failure(
      'INVALID_STATE',
      `task ${id} is already done and cannot be canceled. Use task:update if you only need to adjust metadata.`,
    );
  }

  if (isDryRun(parsed)) {
    return success({
      dry_run: true,
      task: buildCanceledTask(task) as unknown as Record<string, unknown>,
    });
  }

  await storage.withLock(async () => {
    const latest = await storage.readStore('tasks');
    const target = latest.find((entry) => entry.id === id);
    if (!target) {
      return;
    }
    Object.assign(target, buildCanceledTask(target));
    await storage.writeStore('tasks', latest);
    await storage.appendAudit(
      buildAuditEvent({
        tool: TOOL_NAMES.CANCEL,
        op: 'update',
        context,
        ids: [id],
      }),
    );
  });

  const updated = await storage.readStore('tasks');
  const canceledTask = updated.find((entry) => entry.id === id);
  return success({ task: canceledTask as unknown as Record<string, unknown> });
}

// ---------------------------------------------------------------------------
// task:schedule
// ---------------------------------------------------------------------------

async function taskScheduleTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const id = asNonEmptyString(parsed.id);

  if (!id) {
    return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
  }

  const storage = getStoragePort();
  const tasks = await storage.readStore('tasks');
  const task = tasks.find((t) => t.id === id);

  if (!task) {
    return failure('NOT_FOUND', `task ${id} was not found.`);
  }

  const dueAt = asNonEmptyString(parsed.due_at);
  const cron = asNonEmptyString(parsed.cron);
  const previousDueAt = task.due_at ?? null;

  if (isDryRun(parsed)) {
    const preview = {
      ...task,
      ...(dueAt ? { due_at: dueAt } : {}),
      ...(cron ? { cron } : {}),
    };
    return success({
      dry_run: true,
      task: preview as unknown as Record<string, unknown>,
    });
  }

  await storage.withLock(async () => {
    const latest = await storage.readStore('tasks');
    const target = latest.find((t) => t.id === id);
    if (target) {
      if (dueAt) {
        target.due_at = dueAt;
      }
      if (cron) {
        target.cron = cron;
      }
      target.updated_at = nowIso();
      await storage.writeStore('tasks', latest);
      await storage.appendAudit(
        buildAuditEvent({
          tool: TOOL_NAMES.SCHEDULE,
          op: 'update',
          context,
          ids: [id],
        }),
      );
    }
  });

  const updated = await storage.readStore('tasks');
  const scheduledTask = updated.find((t) => t.id === id);
  if (scheduledTask) {
    await emitSidekickEvent(buildTaskScheduledEvent(scheduledTask));
    if (movedDueDateLater(previousDueAt ?? undefined, scheduledTask.due_at)) {
      await emitSidekickEvent(
        buildTaskSnoozedEvent({
          task_id: id,
          previous_due_at: previousDueAt,
          due_at: scheduledTask.due_at ?? previousDueAt ?? '',
        }),
      );
    }
  }

  return success({ task: scheduledTask as unknown as Record<string, unknown> });
}

// ---------------------------------------------------------------------------
// Router (default export)
// ---------------------------------------------------------------------------

export default async function taskTools(
  input: unknown,
  context?: ToolContextLike,
): Promise<ToolOutput> {
  switch (context?.tool_name) {
    case TOOL_NAMES.CREATE:
      return taskCreateTool(input, context);
    case TOOL_NAMES.LIST:
      return taskListTool(input, context);
    case TOOL_NAMES.UPDATE:
      return taskUpdateTool(input, context);
    case TOOL_NAMES.CANCEL:
      return taskCancelTool(input, context);
    case TOOL_NAMES.COMPLETE:
      return taskCompleteTool(input, context);
    case TOOL_NAMES.SCHEDULE:
      return taskScheduleTool(input, context);
    default:
      return failure('UNKNOWN_TOOL', `Unknown task tool: ${context?.tool_name ?? 'unknown'}`);
  }
}
