/**
 * TITAN — Cron Skill (Built-in)
 * Schedule and manage automated tasks using node-cron.
 * Jobs are persisted to the DB and re-scheduled on gateway boot via initCronScheduler().
 */
import * as cron from 'node-cron';
import { exec } from 'child_process';
import { registerSkill } from '../registry.js';
import { getDb } from '../../memory/memory.js';
import { v4 as uuid } from 'uuid';
import logger from '../../utils/logger.js';

const COMPONENT = 'Cron';

// ─── Active task handles ────────────────────────────────────────
// Maps job ID → node-cron ScheduledTask so we can stop/restart them.
const activeTasks = new Map<string, ReturnType<typeof cron.schedule>>();

// ─── Shell execution helper ─────────────────────────────────────

/** Execute a shell command and return its output (stdout + stderr). */
function executeCommand(command: string, timeout: number = 60000): Promise<string> {
    return new Promise((resolve) => {
        const proc = exec(
            command,
            {
                timeout,
                maxBuffer: 1024 * 1024 * 5, // 5 MB
                shell: '/bin/bash',
            },
            (error, stdout, stderr) => {
                let output = '';
                if (stdout) output += stdout;
                if (stderr) output += (output ? '\n' : '') + `[stderr] ${stderr}`;
                if (error) {
                    if (error.killed) {
                        output += (output ? '\n' : '') + `[timed out after ${timeout}ms]`;
                    } else {
                        output += (output ? '\n' : '') + `[exit code: ${error.code}]`;
                    }
                }
                // Truncate very long output so we don't bloat logs
                if (output.length > 10000) {
                    output = output.slice(0, 5000) + '\n... [truncated] ...\n' + output.slice(-5000);
                }
                resolve(output || '(no output)');
            },
        );
        void proc; // satisfy TS — proc is managed by exec callback
    });
}

/**
 * v6.0.5 — Send the command as an LLM prompt to the agent loop. This is
 * the `mode: "tool"` path. Previously the scheduler ignored the mode flag
 * and ALL jobs (including tool-mode ones with English prompts) were
 * shoved through bash, which mangled FB posts among other things.
 */
async function executePrompt(prompt: string, allowedTools?: string[] | string): Promise<string> {
    try {
        const { processMessage } = await import('../../agent/agent.js');
        // sessionId 'cron-<rand>' so the agent loop doesn't try to recall
        // chat history. userId 'cron' identifies the surface in logs.
        const sessionId = `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
        // v6.4.4: normalize allowedTools. Legacy DB entries (anything created
        // before this ship) store this as a comma-separated string like
        // "fb_post,fb_read_feed" instead of an array. Accept both shapes so we
        // don't need a DB migration.
        const normalized: string[] | undefined = Array.isArray(allowedTools)
            ? allowedTools
            : (typeof allowedTools === 'string' && allowedTools.length > 0
                ? allowedTools.split(',').map(s => s.trim()).filter(Boolean)
                : undefined);
        // v6.4.4: allowedTools is now honored end-to-end. processMessage's
        // overrides type accepts it; when present, the agent loop short-
        // circuits the persona / brain / toolSearch / dangerous filter
        // chain and binds EXACTLY these tools. This is what cron jobs
        // like fb-* need to make the model actually call fb_post instead
        // of hallucinating <tool_call name="fb_post"> in the response text.
        // (channel='cron' is already passed as the 3rd positional arg above.)
        const result = await processMessage(prompt, sessionId, 'cron',
            normalized && normalized.length > 0 ? { allowedTools: normalized } : undefined);
        const summary = (result.content || '').slice(0, 500);
        const tools = (result.toolsUsed || []).join(', ');
        return `[tool-mode] used tools: ${tools || '(none)'}\n${summary}`;
    } catch (err) {
        return `[tool-mode error] ${(err as Error).message}`;
    }
}

/**
 * v6.0.5 — Defensive guard for shell-mode jobs. Refuses commands that
 * clearly look like English prose / LLM prompts rather than shell.
 *
 * The trigger for this guard: FB autopilot cron jobs were created with
 * the agent prompt as the `command` field but with `mode` left at the
 * default ("shell"). Because the scheduler also ignored `mode`, those
 * prompts went straight into `/bin/bash -c "You are TITAN..."` every day,
 * resulting in mangled / broken output. We catch the "looks like a
 * prompt" case BEFORE bash sees it so the only fix needed is the
 * scheduler honoring mode.
 *
 * Heuristic: starts with words like "you are", "post a", "write a",
 * "build a", or contains "Use the X tool" — strong signals of an LLM
 * prompt accidentally routed here. Conservative; real shell commands
 * (cd, ls, git, npm, python, node, /bin/, ./, ~, $(...) , etc.) never
 * match.
 */
function looksLikeLlmPrompt(cmd: string): boolean {
    const head = cmd.trim().slice(0, 200).toLowerCase();
    const promptStarts = [
        'you are ', 'you are titan',
        'post a ', 'post an ',
        'write a ', 'write an ',
        'build a ', 'build me ', 'build the ',
        'create a ', 'create me ',
        'generate a ', 'generate an ',
        'compose a ', 'compose an ',
        'draft a ', 'draft an ',
    ];
    if (promptStarts.some(p => head.startsWith(p))) return true;
    if (/\buse the (?:[a-z_]+) tool\b/i.test(cmd)) return true;
    if (/\bunder \d+ words\b/i.test(cmd)) return true;
    return false;
}

// ─── Scheduler helpers ──────────────────────────────────────────

/**
 * Start a node-cron task for a persisted job. Returns the task handle.
 *
 * v6.0.5 — Now honors `mode` (shell vs tool). Previously this signature
 * was `(jobId, schedule, command)` and ALL jobs were bash-executed
 * regardless of `record.mode`. That bug mangled the FB autopilot cron
 * jobs (English prompts routed to /bin/bash). The signature now takes
 * a record-shaped arg so callers don't have to remember to pass mode +
 * allowedTools separately, and forgetting the mode flag silently
 * defaults to shell at most, never bypasses it.
 */
interface CronJobLike {
    id: string;
    name?: string;
    schedule: string;
    command: string;
    mode?: 'shell' | 'tool';
    allowedTools?: string[];
}

function scheduleJob(job: CronJobLike): ReturnType<typeof cron.schedule> | null {
    if (!cron.validate(job.schedule)) {
        logger.warn(COMPONENT, `Invalid cron expression for job ${job.id}: "${job.schedule}"`);
        return null;
    }

    const mode: 'shell' | 'tool' = job.mode === 'tool' ? 'tool' : 'shell';
    const task = cron.schedule(job.schedule, async () => {
        logger.info(COMPONENT, `Running cron job ${job.id} (mode=${mode}, name=${job.name ?? '(unnamed)'}): ${job.command.slice(0, 120)}`);

        const store = getDb();
        const record = store.cronJobs.find((j) => j.id === job.id);
        if (record) record.last_run = new Date().toISOString();

        try {
            let output: string;
            if (mode === 'tool') {
                output = await executePrompt(job.command, job.allowedTools);
            } else if (looksLikeLlmPrompt(job.command)) {
                // Defensive guard for "shell" mode that's actually an LLM
                // prompt. Refuse instead of letting bash mangle it.
                output = `[refused] This shell-mode cron job looks like an LLM prompt, not a shell command. Re-create the job with mode:"tool" so it routes to the agent loop. Command head: ${job.command.slice(0, 80)}…`;
                logger.warn(COMPONENT, `Cron job ${job.id}: REFUSED to bash-execute LLM-prompt-shaped command`);
            } else {
                output = await executeCommand(job.command);
            }
            logger.info(COMPONENT, `Cron job ${job.id} completed:\n${output.slice(0, 500)}`);
        } catch (err) {
            logger.error(COMPONENT, `Cron job ${job.id} failed: ${(err as Error).message}`);
        }
    });

    return task;
}

/** Stop and remove a task from the active map. */
function stopAndRemoveTask(jobId: string): void {
    const task = activeTasks.get(jobId);
    if (task) {
        task.stop();
        activeTasks.delete(jobId);
        logger.debug(COMPONENT, `Stopped cron task: ${jobId}`);
    }
}

// ─── Public: init scheduler on gateway boot ─────────────────────

/**
 * Called once from server.ts on startup.
 * Reads all enabled cron jobs from the DB and schedules them.
 */
export function initCronScheduler(): void {
    const store = getDb();
    let scheduled = 0;
    let skipped = 0;

    for (const job of store.cronJobs) {
        if (!job.enabled) { skipped++; continue; }

        const task = scheduleJob(job);
        if (task) {
            activeTasks.set(job.id, task);
            scheduled++;
            logger.debug(COMPONENT, `Scheduled: "${job.name}" (${job.schedule}, mode=${job.mode ?? 'shell'})`);
        } else {
            skipped++;
        }
    }

    logger.info(COMPONENT, `Cron scheduler initialised — ${scheduled} active, ${skipped} skipped`);
}

// ─── Skill registration ─────────────────────────────────────────

export function registerCronSkill(): void {
    registerSkill(
        { name: 'cron', description: 'Schedule and manage recurring tasks using cron expressions. USE THIS WHEN Tony says: "schedule this to run every X", "set up a recurring task", "run this daily at 9am", "run this every 5 minutes", "add a cron job", "automate X on a schedule". WORKFLOW: action=create with name, schedule (cron expression), and shell command — action=list to see all jobs — action=enable/disable to toggle — action=delete to remove.', version: '1.1.0', source: 'bundled', enabled: true },
        {
            name: 'cron',
            description: `Schedule, list, toggle, or delete recurring jobs. Jobs persist across restarts and run via node-cron in-process. Each job has a name, a cron expression, a command, and an execution mode (shell or tool).

USE WHEN: "schedule this to run every X" / "set up a recurring task" / "run this daily at 9am" / "every 5 minutes do X" / "add a cron job" / "automate X on a schedule" / "stop that cron job".

DO NOT USE FOR:
- One-off tasks → just run them now via shell or the appropriate tool.
- Tasks triggered by events (file change, webhook) → use event_triggers / webhook respectively.
- Tasks that need TITAN to "wake up" and decide what to do → set mode:"tool" so the cron sends a message to the agent loop instead of running a bare shell.

Parameters:
- action (string, required) — one of: "create", "list", "delete", "enable", "disable".
- name (string, required for create) — human-readable label.
- schedule (string, required for create) — cron expression. "0 9 * * *" = daily at 9am, "*/5 * * * *" = every 5 minutes, "0 0 * * 0" = Sunday midnight. Five fields, no seconds.
- command (string, required for create) — shell command OR message to TITAN (depending on mode).
- mode (string, optional) — "shell" (default; runs as raw shell) or "tool" (sent as a message to the agent loop, gets all tools).
- allowedTools (string, optional) — comma-separated tool allowlist for mode:"tool" jobs.
- jobId (string, required for delete/enable/disable) — id from list.

Returns:
- create → "Created job: <id>"
- list → table of all jobs with status, next-run, last-run.
- delete/enable/disable → confirmation with the affected jobId.

Errors:
- "Invalid cron expression" — five fields required, no seconds. Test at crontab.guru.
- "Job not found: <id>" — call list to discover the right id.
- "Tool not in allowlist" (mode:"tool") — extend allowedTools or remove the constraint.`,
            parameters: {
                type: 'object',
                properties: {
                    action: {
                        type: 'string',
                        enum: ['create', 'list', 'delete', 'enable', 'disable'],
                        description: 'Action to perform',
                    },
                    name: { type: 'string', description: 'Human-readable name for the cron job' },
                    schedule: {
                        type: 'string',
                        description: 'Cron schedule expression (e.g., "0 9 * * *" for daily at 9 am, "*/5 * * * *" every 5 min)',
                    },
                    command: { type: 'string', description: 'Shell command or task description to execute when the job runs' },
                    jobId: { type: 'string', description: 'Job ID (required for delete/enable/disable)' },
                    mode: { type: 'string', enum: ['shell', 'tool'], description: 'Execution mode: "shell" (default) runs a shell command, "tool" sends the command as a message to TITAN for tool-based execution' },
                    allowedTools: { type: 'string', description: 'Comma-separated list of allowed tool names for tool-mode jobs (e.g., "web_search,write_file,shell")' },
                },
                required: ['action'],
            },
            execute: async (args) => {
                const action = args.action as string;
                const store = getDb();

                switch (action) {
                    // ── Create ─────────────────────────────────
                    case 'create': {
                        const name = args.name as string;
                        const schedule = args.schedule as string;
                        const command = args.command as string;

                        if (!name || !schedule || !command) {
                            return 'Error: name, schedule, and command are required for create';
                        }

                        if (!cron.validate(schedule)) {
                            return `Error: "${schedule}" is not a valid cron expression. Example: "0 9 * * *" (daily at 9 am)`;
                        }

                        const mode = (args.mode as string) === 'tool' ? 'tool' : 'shell';
                        const allowedToolsStr = args.allowedTools as string | undefined;
                        const allowedTools = allowedToolsStr ? allowedToolsStr.split(',').map(t => t.trim()).filter(Boolean) : undefined;

                        const id = uuid();
                        const record = {
                            id,
                            name,
                            schedule,
                            command,
                            mode: mode as 'shell' | 'tool',
                            allowedTools,
                            enabled: true,
                            created_at: new Date().toISOString(),
                        };
                        store.cronJobs.push(record);

                        // Schedule immediately so it runs without a restart.
                        // v6.0.5 — Pass the WHOLE record so scheduleJob can
                        // dispatch on mode + honor allowedTools.
                        const task = scheduleJob(record);
                        if (task) {
                            activeTasks.set(id, task);
                        }

                        logger.info(COMPONENT, `Created and scheduled cron job: ${name} (${schedule}, mode=${mode})`);
                        return `Created cron job "${name}" (ID: ${id})\nSchedule: ${schedule}\nMode: ${mode}\nCommand: ${command}\nStatus: Active and running`;
                    }

                    // ── List ──────────────────────────────────
                    case 'list': {
                        if (store.cronJobs.length === 0) return 'No cron jobs configured.';

                        return store.cronJobs.map((j) => {
                            const running = activeTasks.has(j.id) ? '(running)' : '(not scheduled)';
                            const lastRun = j.last_run ? `\n  Last run: ${j.last_run}` : '';
                            return [
                                `• ${j.name} [${j.enabled ? '✅ enabled' : '❌ disabled'}] ${running}`,
                                `  ID: ${j.id}`,
                                `  Schedule: ${j.schedule}`,
                                `  Command: ${j.command}`,
                                lastRun,
                            ].filter(Boolean).join('\n');
                        }).join('\n\n');
                    }

                    // ── Delete ────────────────────────────────
                    case 'delete': {
                        const jobId = args.jobId as string;
                        if (!jobId) return 'Error: jobId is required';

                        const job = store.cronJobs.find((j) => j.id === jobId);
                        if (!job) return `Error: no cron job found with ID: ${jobId}`;

                        // Stop the running task first
                        stopAndRemoveTask(jobId);

                        store.cronJobs = store.cronJobs.filter((j) => j.id !== jobId);
                        logger.info(COMPONENT, `Deleted cron job: ${job.name} (${jobId})`);
                        return `Deleted cron job "${job.name}" (ID: ${jobId})`;
                    }

                    // ── Enable ────────────────────────────────
                    case 'enable': {
                        const eId = args.jobId as string;
                        if (!eId) return 'Error: jobId is required';

                        const job = store.cronJobs.find((j) => j.id === eId);
                        if (!job) return `Error: no cron job found with ID: ${eId}`;

                        job.enabled = true;

                        // Start scheduling if not already active.
                        // v6.0.5 — Pass the whole record so mode is honored.
                        if (!activeTasks.has(eId)) {
                            const task = scheduleJob(job);
                            if (task) {
                                activeTasks.set(eId, task);
                            }
                        }

                        logger.info(COMPONENT, `Enabled cron job: ${job.name} (${eId})`);
                        return `Enabled cron job "${job.name}" — it will now run on schedule (${job.schedule})`;
                    }

                    // ── Disable ───────────────────────────────
                    case 'disable': {
                        const dId = args.jobId as string;
                        if (!dId) return 'Error: jobId is required';

                        const job = store.cronJobs.find((j) => j.id === dId);
                        if (!job) return `Error: no cron job found with ID: ${dId}`;

                        job.enabled = false;

                        // Stop the running task
                        stopAndRemoveTask(dId);

                        logger.info(COMPONENT, `Disabled cron job: ${job.name} (${dId})`);
                        return `Disabled cron job "${job.name}" — it will no longer run until re-enabled`;
                    }

                    default:
                        return `Unknown action: ${action}. Valid actions: create, list, delete, enable, disable`;
                }
            },
        },
    );
}
