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

import { getStoragePort, type RoutineRecord, type RoutineStepRecord } from './storage.js';
import {
  asInteger,
  asNonEmptyString,
  buildAuditEvent,
  createId,
  failure,
  isDryRun,
  nowIso,
  success,
  toRecord,
  type ToolContextLike,
  type ToolOutput,
} from './shared.js';

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

const TOOL_NAMES = {
  CREATE: 'routine:create',
  LIST: 'routine:list',
  UPDATE: 'routine:update',
  DELETE: 'routine:delete',
  RUN: 'routine:run',
} as const;
const ROUTINE_ID_REQUIRED_MESSAGE = 'id is required.';

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

interface NormalizeStepsResult {
  steps: RoutineStepRecord[];
  warnings: string[];
}

function normalizeSteps(value: unknown): NormalizeStepsResult {
  if (!Array.isArray(value)) {
    return { steps: [], warnings: [] };
  }

  const steps: RoutineStepRecord[] = [];
  const warnings: string[] = [];

  for (let i = 0; i < value.length; i++) {
    const candidate = value[i];

    // String shorthand: coerce "tool:name" → { tool: "tool:name", input: {} }
    if (typeof candidate === 'string') {
      const tool = asNonEmptyString(candidate);
      if (tool) {
        steps.push({ tool, input: {} });
      } else {
        warnings.push(`steps[${i}] invalid: expected non-empty string or object with "tool".`);
      }
      continue;
    }

    if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
      warnings.push(`steps[${i}] invalid: expected object with "tool" or a tool name string.`);
      continue;
    }

    const record = candidate as Record<string, unknown>;
    const tool = asNonEmptyString(record.tool);
    if (!tool) {
      warnings.push(`steps[${i}] invalid: missing or empty "tool" property.`);
      continue;
    }

    const input =
      record.input && typeof record.input === 'object' && !Array.isArray(record.input)
        ? (record.input as Record<string, unknown>)
        : {};

    steps.push({ tool, input });
  }

  return { steps, warnings };
}

function asOptionalBoolean(value: unknown): boolean | null {
  if (value === true || value === false) {
    return value;
  }
  return null;
}

// ---------------------------------------------------------------------------
// routine:create
// ---------------------------------------------------------------------------

async function routineCreateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const name = asNonEmptyString(parsed.name);
  const { steps, warnings } = normalizeSteps(parsed.steps);

  if (!name) {
    return failure('INVALID_INPUT', 'name is required.');
  }
  if (steps.length === 0) {
    const detail =
      warnings.length > 0
        ? `steps must include at least one tool step. Issues: ${warnings.join('; ')}`
        : 'steps must include at least one tool step.';
    return failure('INVALID_INPUT', detail);
  }

  const now = nowIso();
  const routine: RoutineRecord = {
    id: createId('routine'),
    name,
    steps,
    cron: asNonEmptyString(parsed.cron) ?? undefined,
    enabled: asOptionalBoolean(parsed.enabled) ?? true,
    created_at: now,
    updated_at: now,
  };

  if (isDryRun(parsed)) {
    return success({
      dry_run: true,
      routine: routine as unknown as Record<string, unknown>,
      ...(warnings.length > 0 ? { warnings } : {}),
    });
  }

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

  return success({
    routine: routine as unknown as Record<string, unknown>,
    ...(warnings.length > 0 ? { warnings } : {}),
  });
}

// ---------------------------------------------------------------------------
// routine:list
// ---------------------------------------------------------------------------

async function routineListTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
  const parsed = toRecord(input);
  const limit = asInteger(parsed.limit);
  const enabledOnly = parsed.enabled_only === true;

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

  const filtered = enabledOnly ? routines.filter((routine) => routine.enabled) : routines;
  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,
  });
}

// ---------------------------------------------------------------------------
// routine:update
// ---------------------------------------------------------------------------

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

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

  const patch: Partial<RoutineRecord> = {};
  const name = asNonEmptyString(parsed.name);
  const cron =
    typeof parsed.cron === 'string' ? (asNonEmptyString(parsed.cron) ?? undefined) : null;
  const enabled = asOptionalBoolean(parsed.enabled);

  if (name) {
    patch.name = name;
  }
  if (cron !== null) {
    patch.cron = cron;
  }
  if (enabled !== null) {
    patch.enabled = enabled;
  }
  if (parsed.steps !== undefined) {
    const { steps, warnings } = normalizeSteps(parsed.steps);
    if (steps.length === 0) {
      const detail =
        warnings.length > 0
          ? `steps must include at least one tool step. Issues: ${warnings.join('; ')}`
          : 'steps must include at least one tool step.';
      return failure('INVALID_INPUT', detail);
    }
    patch.steps = steps;
  }

  if (Object.keys(patch).length === 0) {
    return failure(
      'INVALID_INPUT',
      'routine:update requires at least one of name, steps, cron, or enabled.',
    );
  }

  const storage = getStoragePort();
  const routines = await storage.readStore('routines');
  const routine = routines.find((entry) => entry.id === id);

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

  const preview: RoutineRecord = {
    ...routine,
    ...patch,
    updated_at: nowIso(),
  };

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

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

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

// ---------------------------------------------------------------------------
// routine:delete
// ---------------------------------------------------------------------------

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

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

  const storage = getStoragePort();
  const routines = await storage.readStore('routines');
  const routine = routines.find((entry) => entry.id === id);

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

  if (isDryRun(parsed)) {
    return success({
      dry_run: true,
      deleted_id: id,
    });
  }

  await storage.withLock(async () => {
    const latest = await storage.readStore('routines');
    await storage.writeStore(
      'routines',
      latest.filter((entry) => entry.id !== id),
    );
    await storage.appendAudit(
      buildAuditEvent({
        tool: TOOL_NAMES.DELETE,
        op: 'delete',
        context,
        ids: [id],
      }),
    );
  });

  return success({ deleted_id: id });
}

// ---------------------------------------------------------------------------
// routine:run (PLAN-ONLY -- does NOT execute tool steps)
// ---------------------------------------------------------------------------

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

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

  const storage = getStoragePort();
  const routines = await storage.readStore('routines');
  const routine = routines.find((r) => r.id === id);

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

  await storage.appendAudit(
    buildAuditEvent({
      tool: TOOL_NAMES.RUN,
      op: 'execute',
      context,
      ids: [id],
      details: { plan_only: true },
    }),
  );

  return success({
    routine_id: routine.id,
    name: routine.name,
    plan_only: true,
    plan: routine.steps.map((step, index) => ({
      index,
      tool: step.tool,
      input: step.input,
    })),
    governance: {
      dispatch_required: true,
      execution: 'No tool steps were executed by routine:run. This endpoint only returns a plan.',
    },
  });
}

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

export default async function routineTools(
  input: unknown,
  context?: ToolContextLike,
): Promise<ToolOutput> {
  switch (context?.tool_name) {
    case TOOL_NAMES.CREATE:
      return routineCreateTool(input, context);
    case TOOL_NAMES.LIST:
      return routineListTool(input, context);
    case TOOL_NAMES.UPDATE:
      return routineUpdateTool(input, context);
    case TOOL_NAMES.DELETE:
      return routineDeleteTool(input, context);
    case TOOL_NAMES.RUN:
      return routineRunTool(input, context);
    default:
      return failure('UNKNOWN_TOOL', `Unknown routine tool: ${context?.tool_name ?? 'unknown'}`);
  }
}
