/** Automation tools — Forge's two recurring-work engines:
 *   • Schedules: fire ONE body (pipeline or prompt) on a trigger, optional notify.
 *   • Jobs:      poll a connector, dedup, and dispatch MANY items to pipelines/chat.
 *  Schedules have list/create/update/manage; jobs have list/manage. The
 *  issue-autofix / watch subsystems are deferred to a later phase. In-process lib
 *  calls; run/cancel mirror the route handlers. */

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import {
  listSchedules, getSchedule, createSchedule, updateSchedule, deleteSchedule,
  listScheduleRuns, listInflightForSchedule,
} from '@/lib/schedules/store';
import type { UpdateScheduleInput } from '@/lib/schedules/types';
import { executeSchedule, isScheduleBusy } from '@/lib/schedules/scheduler';
import {
  listJobs, getJob, updateJob, deleteJob,
  listRuns, resetDedup, cancelJobDrain, listJobDispatchedPipelines,
  seedNextRunAt as seedJobNextRunAt,
} from '@/lib/jobs/store';
import { runJob, isJobBusy } from '@/lib/jobs/scheduler';
import { cancelPipeline, getWorkflow } from '@/lib/pipeline';
import { cancelTask } from '@/lib/task-manager';
import { ensureInitialized } from '@/lib/init';
import { text, fail, guard, describeTiming } from './_shared';

const SCHED_ACTIONS = ['run', 'pause', 'resume', 'cancel', 'delete'] as const;
const JOB_ACTIONS = ['run', 'pause', 'resume', 'cancel', 'reset_dedup', 'delete'] as const;

export function registerAutomationTools(server: McpServer): void {
  // ── Schedules ───────────────────────────────────────────────
  server.tool(
    'forge_list_schedules',
    'List Forge schedules (recurring runs of a pipeline or prompt). Pass id for one schedule\'s detail + recent runs. Use forge_manage_schedule to run/pause/resume/cancel/delete.',
    { id: z.string().optional().describe('A schedule id for full detail + recent runs') },
    (params) => guard(() => {
      if (params.id) {
        const s = getSchedule(params.id);
        if (!s) return fail(`Schedule not found: ${params.id}`);
        const busy = isScheduleBusy(s.id);
        const lines = [
          `Schedule: ${s.name} (${s.id}) [${s.enabled ? 'enabled' : 'disabled'}${busy.busy ? `, busy: ${busy.reason}` : ''}]`,
          `Body: ${s.body_kind} → ${s.body_ref}`,
          `Trigger: ${describeTiming(s)} · Action: ${s.action_kind}`,
          `Next run: ${s.next_run_at || 'n/a'} · Last run: ${s.last_run_at || 'never'}`,
        ];
        const runs = listScheduleRuns(s.id, 5);
        if (runs.length) {
          lines.push('Recent runs:');
          for (const r of runs) lines.push(`  ${r.status} ${r.started_at?.slice(0, 16) || ''}${r.error ? ` — ${r.error}` : ''}`);
        }
        return text(lines.join('\n'));
      }
      const all = listSchedules();
      if (!all.length) return text('No schedules.');
      const lines = all.map((s) =>
        `• ${s.name} (${s.id}) [${s.enabled ? 'on' : 'off'}] ${s.body_kind}:${s.body_ref} · ${describeTiming(s)} · next ${s.next_run_at?.slice(0, 16) || 'n/a'}`);
      return text(`${all.length} schedule(s):\n${lines.join('\n')}`);
    }),
  );

  server.tool(
    'forge_manage_schedule',
    'Manage an existing schedule: run (fire now), pause (disable + cancel any in-flight run), resume (enable), cancel (stop in-flight runs — pipeline or prompt-bodied task), or delete.',
    {
      id: z.string().describe('Schedule id (from forge_list_schedules)'),
      action: z.enum(SCHED_ACTIONS).describe('What to do'),
    },
    (params) => guard(async () => {
      const s = getSchedule(params.id);
      if (!s) return fail(`Schedule not found: ${params.id}`);
      const cancelInflight = () => {
        let n = 0;
        for (const r of listInflightForSchedule(s.id)) {
          try {
            // A prompt-bodied run stores target_id = task.id (not a pipeline id),
            // so cancel the right thing by body kind.
            const ok = s.body_kind === 'prompt' ? cancelTask(r.target_id) : cancelPipeline(r.target_id);
            if (ok) n++;
          } catch { /* best-effort */ }
        }
        return n;
      };
      switch (params.action) {
        case 'run': {
          const busy = isScheduleBusy(s.id);
          if (busy.busy) return fail(`Schedule is busy: ${busy.reason}. Cancel it first, or wait.`);
          ensureInitialized();
          const targetId = await executeSchedule(s, 'manual');
          return text(`Fired ${s.name} (manual) → ${s.body_kind} ${targetId}. Track with forge_get_pipeline / forge_get_task id=${targetId}.`);
        }
        case 'pause': { updateSchedule(s.id, { enabled: false }); const n = cancelInflight(); return text(`Paused ${s.name}${n ? ` and cancelled ${n} inflight run(s)` : ''}.`); }
        // resume: updateSchedule reseeds next_run_at on enable (schedules store), so no explicit seed (unlike jobs).
        case 'resume': { updateSchedule(s.id, { enabled: true }); return text(`Resumed ${s.name}.`); }
        case 'cancel': { const n = cancelInflight(); return text(`Cancelled ${n} inflight run(s) for ${s.name}.`); }
        case 'delete': { cancelInflight(); deleteSchedule(s.id); return text(`Deleted ${s.name}.`); }
      }
    }),
  );

  server.tool(
    'forge_upsert_schedule',
    'Create a schedule, or edit an existing one when id is given. CREATE: name + workflow + exactly one trigger (every_minutes / at / cron) are required. EDIT (id set): every field is optional and only what you pass changes (at most one trigger). Optionally notify on completion via action + action_config.',
    {
      id: z.string().optional().describe('Schedule id to EDIT (from forge_list_schedules). Omit to CREATE.'),
      name: z.string().optional().describe('Schedule name (required on create)'),
      workflow: z.string().optional().describe('Pipeline workflow name from forge_list_workflows (required on create)'),
      every_minutes: z.number().int().positive().optional().describe('Interval trigger: run every N minutes'),
      at: z.string().optional().describe('Once trigger: ISO timestamp to fire at (e.g. "2026-07-01T09:00:00Z")'),
      cron: z.string().optional().describe('Cron trigger: a 5-field cron expression (e.g. "0 9 * * *")'),
      input: z.record(z.string(), z.string()).optional().describe('Workflow input variables (replaces existing on edit)'),
      skills: z.array(z.string()).optional().describe('Extra skill names to load for the run'),
      enabled: z.boolean().optional().describe('On create: start enabled (default true). Use forge_manage_schedule to enable/disable later.'),
      action: z.enum(['none', 'chat', 'email', 'telegram']).optional().describe('Notify on completion. Non-none requires action_config.'),
      action_config: z.record(z.string(), z.string()).optional().describe('Config for action — chat: {session_id}; email: {to}; telegram: {chat_id}.'),
    },
    (params) => guard(() => {
      const triggerCount = [params.every_minutes != null, !!params.at, !!params.cron].filter(Boolean).length;
      if (params.action && params.action !== 'none' && !(params.action_config && Object.keys(params.action_config).length)) {
        return fail(`action="${params.action}" needs action_config (chat: session_id; email: to; telegram: chat_id).`);
      }
      const kindFrom = () => params.every_minutes != null ? 'period' as const : params.at ? 'once' as const : 'cron' as const;
      ensureInitialized(); // ensure the scheduler is running so the schedule fires

      if (params.id) { // ── edit ──
        const s = getSchedule(params.id);
        if (!s) return fail(`Schedule not found: ${params.id}`);
        if (triggerCount > 1) return fail('Provide at most one of every_minutes, at, or cron.');
        const patch: UpdateScheduleInput = {};
        if (params.name !== undefined) patch.name = params.name;
        if (params.input !== undefined) patch.input = params.input;
        if (params.skills !== undefined) patch.skills = params.skills;
        if (params.action !== undefined) patch.action_kind = params.action;
        if (params.action_config !== undefined) patch.action_config = params.action_config;
        if (triggerCount === 1) {
          const kind = kindFrom();
          patch.schedule_kind = kind;
          if (kind === 'period') patch.schedule_interval_minutes = params.every_minutes;
          else if (kind === 'once') patch.schedule_at = params.at;
          else patch.schedule_cron = params.cron;
        }
        if (Object.keys(patch).length === 0) return fail('Nothing to update — provide name, input, skills, action, or a trigger (every_minutes/at/cron).');
        updateSchedule(s.id, patch);
        // updateSchedule reseeds next_run_at internally when the trigger changes.
        const after = getSchedule(s.id)!;
        return text(`Updated ${after.name} (${after.id}) — ${describeTiming(after)}. Next run: ${after.next_run_at || 'n/a'}.`);
      }

      // ── create ──
      if (!params.name) return fail('name is required to create a schedule (or pass id to edit one).');
      if (!params.workflow) return fail('workflow is required to create a schedule. Use forge_list_workflows.');
      if (triggerCount !== 1) return fail('Provide exactly one of every_minutes, at, or cron.');
      if (!getWorkflow(params.workflow)) return fail(`Workflow not found: ${params.workflow}. Use forge_list_workflows.`);
      const s = createSchedule({
        name: params.name,
        body_kind: 'pipeline',
        body_ref: params.workflow,
        input: params.input,
        skills: params.skills,
        enabled: params.enabled,
        schedule_kind: kindFrom(),
        schedule_interval_minutes: params.every_minutes,
        schedule_at: params.at ?? null,
        schedule_cron: params.cron ?? null,
        action_kind: params.action || 'none',
        action_config: params.action_config,
      });
      return text(`Created schedule ${s.name} (${s.id}) — ${describeTiming(s)} → pipeline ${s.body_ref}. Next run: ${s.next_run_at || 'n/a'}.\nManage with forge_manage_schedule id=${s.id}.`);
    }),
  );

  // ── Jobs ────────────────────────────────────────────────────
  server.tool(
    'forge_list_jobs',
    'List Forge jobs (poll a connector on a schedule, dedup results, and dispatch each new item to a pipeline or chat). Pass id for one job\'s detail + recent runs. Use forge_manage_job to run/pause/resume/cancel/reset_dedup/delete.',
    { id: z.string().optional().describe('A job id for full detail + recent runs') },
    (params) => guard(() => {
      if (params.id) {
        const j = getJob(params.id);
        if (!j) return fail(`Job not found: ${params.id}`);
        const busy = isJobBusy(j.id);
        const wf = j.dispatch_type === 'pipeline' ? ` → ${(j.dispatch_params as { workflow_name?: string }).workflow_name || '?'}` : '';
        const lines = [
          `Job: ${j.name} (${j.id}) [${j.enabled ? 'enabled' : 'disabled'}${busy.busy ? `, busy: ${busy.reason}` : ''}]`,
          `Source: ${j.source_connector}.${j.source_tool} (dedup on "${j.dedup_field}")`,
          `Dispatch: ${j.dispatch_type}${wf} · max ${j.max_per_tick}/tick · ${j.concurrency_mode}`,
          `Trigger: ${describeTiming(j)} · Next run: ${j.next_run_at || 'n/a'} · Last: ${j.last_run_at || 'never'}`,
        ];
        const runs = listRuns(j.id, 5);
        if (runs.length) {
          lines.push('Recent runs:');
          for (const r of runs) lines.push(`  ${r.status} seen=${r.items_seen} new=${r.items_new} dispatched=${r.items_dispatched} ${r.started_at?.slice(0, 16) || ''}${r.error ? ` — ${r.error}` : ''}`);
        }
        return text(lines.join('\n'));
      }
      const all = listJobs();
      if (!all.length) return text('No jobs.');
      const lines = all.map((j) =>
        `• ${j.name} (${j.id}) [${j.enabled ? 'on' : 'off'}] ${j.source_connector}.${j.source_tool} → ${j.dispatch_type} · ${describeTiming(j)} · next ${j.next_run_at?.slice(0, 16) || 'n/a'}`);
      return text(`${all.length} job(s):\n${lines.join('\n')}`);
    }),
  );

  server.tool(
    'forge_manage_job',
    'Manage an existing job: run (fire now), pause (disable + stop drain), resume (enable), cancel (stop drain + cancel inflight dispatched pipelines), reset_dedup (re-process all items next run), or delete.',
    {
      id: z.string().describe('Job id (from forge_list_jobs)'),
      action: z.enum(JOB_ACTIONS).describe('What to do'),
    },
    (params) => guard(async () => {
      const j = getJob(params.id);
      if (!j) return fail(`Job not found: ${params.id}`);
      switch (params.action) {
        case 'run': {
          const busy = isJobBusy(j.id);
          if (busy.busy) return fail(`Job is busy: ${busy.reason}. Cancel it first, or wait.`);
          ensureInitialized();
          const runId = await runJob(j.id, 'manual');
          return text(`Ran ${j.name} (manual) → run ${runId}. Inspect with forge_list_jobs id=${j.id}.`);
        }
        case 'pause': { updateJob(j.id, { enabled: false }); cancelJobDrain(j.id); return text(`Paused ${j.name} (drain stopped).`); }
        case 'resume': {
          updateJob(j.id, { enabled: true });
          // reseed next_run_at: pause (cancelJobDrain) nulled it, NULL = due now.
          if (j.schedule_kind !== 'manual') seedJobNextRunAt(j.id);
          return text(`Resumed ${j.name}.`);
        }
        case 'cancel': {
          cancelJobDrain(j.id);
          let n = 0;
          for (const p of listJobDispatchedPipelines(j.id, 50)) {
            if (p.pipeline_status === 'running' || p.pipeline_status === 'pending') {
              try { if (cancelPipeline(p.pipeline_id)) n++; } catch { /* best-effort */ }
            }
          }
          return text(`Stopped drain for ${j.name}${n ? `, cancelled ${n} inflight pipeline(s)` : ''}.`);
        }
        case 'reset_dedup': { const c = resetDedup(j.id); return text(`Reset dedup for ${j.name} (${c} key(s) cleared) — next run re-processes all items.`); }
        case 'delete': { cancelJobDrain(j.id); deleteJob(j.id); return text(`Deleted ${j.name}.`); }
      }
    }),
  );
}
