/**
 * Schedules action runner — fires the schedule's action against the
 * body_output once the body settles.
 *
 * Phase 4 ships action.kind='chat' only. email + telegram land in
 * phases 5-6. action_kind='none' is the default no-op.
 *
 * Wired into every body-completion site:
 *   - pipeline-scheduler.syncRunStatus (pipeline body done)
 *   - scheduler.settleSkillRunIfMatch  (skill body done)
 *   - scheduler.invokeConnectorTool    (connector_tool done)
 *   - store.reconcileStaleScheduleRuns (forge-restart catch-up)
 *
 * Action runs at-most-once per run (we look at action_status='pending'/
 * null and set to 'done'/'failed'/'skipped' atomically).
 */

import { getDb } from '../../src/core/db/database';
import { getDbPath } from '../../src/config';
import { getScheduleRun, getSchedule, setScheduleRunAction } from './store';
import { appendMessage, getSession } from '../chat/session-store';
import { loadSettings } from '../settings';
import type { Schedule, ScheduleRun, ScheduleActionStatus } from './types';

function db() { return getDb(getDbPath()); }

/**
 * Run the action for a settled schedule_run by target_id (the most
 * common entry point — callers know target_id, not run_id). Looks up
 * the latest non-pending row for that target. Safe to call multiple
 * times; idempotent against `action_status`.
 */
export async function runActionForTarget(targetId: string): Promise<void> {
  const row = db().prepare(
    `SELECT id FROM schedule_runs WHERE target_id = ?
       ORDER BY started_at DESC LIMIT 1`,
  ).get(targetId) as { id: string } | undefined;
  if (!row) return;
  await runAction(row.id);
}

/** Run the action against a specific schedule_run. */
export async function runAction(runId: string): Promise<void> {
  const run = getScheduleRun(runId);
  if (!run) return;
  if (run.action_status !== null) return; // already settled

  const schedule = getSchedule(run.schedule_id);
  if (!schedule) {
    markAction(runId, 'failed', 'schedule gone');
    return;
  }

  if (schedule.action_kind === 'none') {
    markAction(runId, 'done', null);
    return;
  }

  // Body must have completed successfully for action to fire.
  // 'failed' / 'cancelled' body: skip action (downstream chat/email
  // shouldn't get a half-baked output).
  if (run.status !== 'done') {
    markAction(runId, 'skipped', `body status was ${run.status}`);
    return;
  }

  const output = (run.body_output || '').trim();
  if (!output && schedule.action_skip_on_empty) {
    markAction(runId, 'skipped', 'body produced no output and action_skip_on_empty=true');
    return;
  }

  try {
    if (schedule.action_kind === 'chat') {
      await runChatAction(schedule, run, output);
      markAction(runId, 'done', null);
    } else if (schedule.action_kind === 'email') {
      await runEmailAction(schedule, run, output);
      markAction(runId, 'done', null);
    } else if (schedule.action_kind === 'telegram') {
      await runTelegramAction(schedule, run, output);
      markAction(runId, 'done', null);
    } else {
      markAction(runId, 'failed', `unknown action_kind '${schedule.action_kind}'`);
    }
  } catch (e) {
    const err = e as Error;
    console.error(`[schedules:action] run ${runId} ${schedule.action_kind} failed:`, err);
    markAction(runId, 'failed', err.message || String(e));
  }
}

function markAction(runId: string, status: ScheduleActionStatus, error: string | null): void {
  setScheduleRunAction({ run_id: runId, status, error });
}

// ─── chat action ──────────────────────────────────────────

async function runChatAction(schedule: Schedule, run: ScheduleRun, output: string): Promise<void> {
  const cfg = schedule.action_config || {};
  const sessionId = typeof cfg.session_id === 'string' ? cfg.session_id.trim() : '';
  if (!sessionId) {
    throw new Error('chat action requires action_config.session_id');
  }
  const session = getSession(sessionId);
  if (!session) {
    throw new Error(`chat session "${sessionId}" not found`);
  }
  const prefix = typeof cfg.prefix === 'string' ? cfg.prefix : '';
  const header = `📋 Schedule "${schedule.name}" — ${new Date(run.started_at).toLocaleString()}`;
  const text = prefix
    ? `${header}\n\n${prefix}${output}`
    : `${header}\n\n${output}`;

  // Route through chat-standalone so an open chat tab gets the SSE
   // `message_saved` push. Writing the row directly via appendMessage
   // works for persistence but the tab only sees the message after a
   // reload. Fall back to direct write if the standalone is down.
  const chatPort = Number(process.env.CHAT_PORT) || 8408;
  const payload = JSON.stringify({ role: 'assistant', blocks: [{ type: 'text', text }] });
  try {
    const r = await fetch(`http://127.0.0.1:${chatPort}/api/sessions/${encodeURIComponent(sessionId)}/inject`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: payload,
    });
    if (!r.ok) throw new Error(`inject HTTP ${r.status}`);
  } catch (e) {
    console.warn(`[schedules:chat] inject via standalone failed (${(e as Error).message}); falling back to direct DB write`);
    appendMessage({
      session_id: sessionId,
      role: 'assistant',
      blocks: [{ type: 'text', text }],
    });
  }
}

// ─── email action ─────────────────────────────────────────

/**
 * Email action via nodemailer. SMTP transport is configured globally
 * in settings.smtp* (host/port/secure/user/password/from). Each
 * schedule supplies the recipient + templates in action_config:
 *
 *   {
 *     "to":               "alice@example.com" | ["a@..", "b@.."],
 *     "subject_template": "Daily bug list — {date}",
 *     "body_template":    "{body_output}",
 *     "html":             false   // (optional) treat body as HTML
 *   }
 *
 * Templates support two placeholders for now:
 *   {date}        — current date (YYYY-MM-DD)
 *   {body_output} — captured body output
 */
async function runEmailAction(schedule: Schedule, _run: ScheduleRun, output: string): Promise<void> {
  const settings = loadSettings();
  if (!settings.smtpHost) {
    throw new Error('SMTP not configured (Settings → SMTP)');
  }
  const cfg = schedule.action_config || {};
  const toRaw = cfg.to;
  const to = Array.isArray(toRaw)
    ? (toRaw as unknown[]).filter((x) => typeof x === 'string' && x.trim()).map((x) => String(x).trim())
    : (typeof toRaw === 'string' && toRaw.trim() ? [toRaw.trim()] : []);
  if (to.length === 0) {
    throw new Error('email action requires action_config.to (string or string[])');
  }

  const today = new Date().toISOString().slice(0, 10);
  const subjectTpl = typeof cfg.subject_template === 'string' && cfg.subject_template
    ? cfg.subject_template
    : `Forge schedule: ${schedule.name}`;
  const bodyTpl = typeof cfg.body_template === 'string' && cfg.body_template
    ? cfg.body_template
    : '{body_output}';
  const subject = subjectTpl.replace('{date}', today).replace('{body_output}', output);
  const bodyText = bodyTpl.replace('{date}', today).replace('{body_output}', output);
  const asHtml = !!cfg.html;

  // Lazy import keeps the SMTP client out of the main bundle until the
  // first email action actually fires.
  const nodemailer = (await import('nodemailer')).default;
  const transport = nodemailer.createTransport({
    host: settings.smtpHost,
    port: settings.smtpPort,
    secure: !!settings.smtpSecure,
    auth: settings.smtpUser
      ? { user: settings.smtpUser, pass: settings.smtpPassword }
      : undefined,
  } as any);
  const from = settings.smtpFrom || (settings.smtpUser ? settings.smtpUser : '');
  if (!from) throw new Error('SMTP from address required (Settings → SMTP → from or user)');
  await transport.sendMail({
    from,
    to,
    subject,
    [asHtml ? 'html' : 'text']: bodyText,
  });
}

// ─── telegram action ──────────────────────────────────────

/**
 * Telegram action — posts body_output via the same bot Forge already
 * uses for task notifications. Reuses settings.telegramBotToken; chat
 * id falls back to settings.telegramChatId so a user can just toggle
 * the radio without per-schedule config.
 *
 * action_config:
 *   chat_id      string (optional, overrides settings.telegramChatId)
 *   prefix       string (optional, prepended to body_output)
 *   parse_mode   "Markdown" | "MarkdownV2" | "HTML" | "" (default Markdown)
 */
async function runTelegramAction(schedule: Schedule, run: ScheduleRun, output: string): Promise<void> {
  const settings = loadSettings();
  if (!settings.telegramBotToken) {
    throw new Error('telegramBotToken not configured (Settings → Telegram)');
  }
  const cfg = schedule.action_config || {};
  const chatId = typeof cfg.chat_id === 'string' && cfg.chat_id.trim()
    ? cfg.chat_id.trim()
    : settings.telegramChatId;
  if (!chatId) {
    throw new Error('telegram action requires action_config.chat_id (or settings.telegramChatId)');
  }
  const prefix = typeof cfg.prefix === 'string' ? cfg.prefix : '';
  const parseMode = typeof cfg.parse_mode === 'string' ? cfg.parse_mode : 'Markdown';
  const header = `📋 *${escapeTelegram(schedule.name)}* — ${new Date(run.started_at).toLocaleString()}`;
  const text = prefix
    ? `${header}\n\n${prefix}${output}`
    : `${header}\n\n${output}`;

  const url = `https://api.telegram.org/bot${settings.telegramBotToken}/sendMessage`;
  const body: Record<string, unknown> = {
    chat_id: chatId,
    text,
    disable_web_page_preview: true,
  };
  if (parseMode) body.parse_mode = parseMode;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errText = await res.text().catch(() => '');
    throw new Error(`telegram sendMessage failed: ${res.status} ${errText.slice(0, 200)}`);
  }
}

/** Escape underscores / asterisks in the schedule name so Markdown
 *  doesn't try to render them. parse_mode='Markdown' is lenient but
 *  unbalanced `_` causes a 400 from the API. */
function escapeTelegram(s: string): string {
  return s.replace(/([_*`\[])/g, '\\$1');
}
