import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
initStderrTee("scheduling");
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { scheduleEvent } from "./tools/schedule-event.js";
import { scheduleList } from "./tools/schedule-list.js";
import { scheduleGet } from "./tools/schedule-get.js";
import { scheduleUpdate } from "./tools/schedule-update.js";
import { scheduleCancel } from "./tools/schedule-cancel.js";
import { scheduleExportIcs } from "./tools/schedule-export-ics.js";
import { scheduleImportIcs } from "./tools/schedule-import-ics.js";
import { scheduleArchiveIcs } from "./tools/schedule-archive-ics.js";
import { closeDriver, getUserTimezone } from "./lib/neo4j.js";
import { formatCombined } from "./lib/time-format.js";
// Cast to `any` to avoid TS2589. TypeScript hits its 50-level instantiation
// limit when accumulating typed tool signatures on a generic McpServer.
// Individual tool handlers retain full type safety in their own files.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const server = new McpServer({
    name: "maxy-scheduling",
    version: "0.1.0",
});
// Plugin-system boot tolerance — see Task 202.
// Module-init must never throw; plugin-system spawn enumerates tools/list
// before ACCOUNT_ID is set. Per-call refusal preserves account isolation.
const accountId = process.env.ACCOUNT_ID ?? null;
process.stderr.write(`[scheduling] boot accountId=${accountId ?? "null"}\n`);
function refuseNoAccount(toolName) {
    process.stderr.write(`[scheduling] tool=${toolName} refuse reason=no-account-context\n`);
    return {
        content: [
            {
                type: "text",
                text: `${toolName} cannot execute: this MCP instance was launched without ACCOUNT_ID (plugin-system metadata-only mode). Per-account scheduling requires ACCOUNT_ID at spawn.`,
            },
        ],
        isError: true,
    };
}
// userId is read at module load and passed to every UserProfile
// read. NOT boot-throw on missing — the silent-compaction path
// (compaction.ts:53) intentionally spawns this MCP without USER_ID for
// summary-only turns that don't invoke scheduling tools. The lib's
// per-call MissingUserIdError handles the actual bug class (caller forgot
// to thread userId on a real tool invocation).
const userId = process.env.USER_ID || "";
const sessionId = process.env.SESSION_ID || undefined;
const agentSlug = process.env.AGENT_SLUG || undefined;
// Task 476 — boot signal so a stale spawn-env contract is visible at the
// first byte of stderr.
process.stderr.write(`[scheduling] boot accountId=${accountId ?? "-"} sessionId=${sessionId ?? "-"} agentSlug=${agentSlug ?? "-"} userId-present=${userId.length > 0 ? "yes" : "no"}\n`);
// ---------------------------------------------------------------------------
// Shared schema fragments
// ---------------------------------------------------------------------------
const eventStatusSchema = z
    .enum(["scheduled", "due", "completed", "cancelled"])
    .optional()
    .describe("Event status");
// ---------------------------------------------------------------------------
// schedule-event
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-event", "Create a scheduled event or appointment. For one-time events, provide startDate. For recurring events, also provide a recurrence (cron expression, e.g. '0 8 * * 1-5' for weekdays at 8am). Pass sessionKey to link to the current conversation. Pass action to auto-execute an MCP tool when the event fires.", {
    name: z.string().describe("Short event title"),
    description: z.string().optional().describe("Event details"),
    startDate: z.string().describe("ISO 8601 datetime for the event start"),
    endDate: z.string().optional().describe("ISO 8601 datetime for the event end"),
    location: z.string().optional().describe("Event location (free text)"),
    recurrence: z.string().optional().describe("Cron expression for recurring events (5-field: min hour dom mon dow). Omit for one-time events."),
    timezone: z.string().optional().describe("IANA timezone (e.g. Europe/London). Defaults to account timezone."),
    sessionKey: z.string().optional().describe("Current session key — links this event to the conversation"),
    action: z.object({
        plugin: z.string().describe("Plugin name that exposes the tool (e.g. 'memory')"),
        tool: z.string().describe("MCP tool name to call when the event fires (e.g. 'memory-search')"),
        args: z.record(z.unknown()).optional().describe("Arguments to pass to the tool call"),
    }).optional().describe("Automated action: dispatches an MCP tool call when the event fires. The plugin must exist in platform/plugins/."),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("schedule-event");
    try {
        const timezone = await getUserTimezone(accountId, userId);
        console.error(`[schedule-event] formatted for timezone: ${timezone}`);
        const result = await scheduleEvent({
            ...params,
            accountId,
            sessionKey: params.sessionKey ?? sessionId,
            createdBy: { agent: agentSlug, session: sessionId, tool: "schedule-event" },
        });
        const parts = [`Event created: "${params.name}" (ID: ${result.eventId})`];
        parts.push(`starts: ${formatCombined(params.startDate, timezone)}`);
        if (params.recurrence)
            parts.push(`recurs: ${params.recurrence}`);
        if (result.nextRun)
            parts.push(`next run: ${formatCombined(result.nextRun, timezone)}`);
        if (params.action)
            parts.push(`action: ${params.action.plugin}/${params.action.tool}`);
        return { content: [{ type: "text", text: parts.join(", ") }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Failed to create event: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-list
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-list", "List scheduled events. Filter by status, date range, or upcoming only. Ordered by next occurrence.", {
    status: eventStatusSchema,
    from: z.string().optional().describe("ISO 8601 date — events starting on or after this date"),
    to: z.string().optional().describe("ISO 8601 date — events starting on or before this date"),
    upcoming: z.boolean().optional().describe("If true, show only scheduled/due events with future dates"),
    limit: z.number().optional().describe("Max results (default 50)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("schedule-list");
    try {
        const timezone = await getUserTimezone(accountId, userId);
        console.error(`[schedule-list] formatted for timezone: ${timezone}`);
        const events = await scheduleList({ ...params, accountId });
        if (events.length === 0) {
            return { content: [{ type: "text", text: "No events match the filters." }] };
        }
        const lines = events.map((e) => {
            const recur = e.recurrence ? ` [recurring: ${e.recurrence}]` : "";
            const loc = e.location ? ` @ ${e.location}` : "";
            const next = e.nextRun ? ` next: ${formatCombined(e.nextRun, timezone)}` : "";
            return `${e.eventStatus.toUpperCase()} — ${e.name} (${formatCombined(e.startDate, timezone)})${loc}${recur}${next} (${e.eventId})`;
        });
        return { content: [{ type: "text", text: `${events.length} event(s):\n\n${lines.join("\n")}` }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `List failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-get
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-get", "Fetch a single event with its full details, notes history, and all graph relationships.", {
    eventId: z.string().describe("Event ID (UUID)"),
}, async ({ eventId }) => {
    if (!accountId)
        return refuseNoAccount("schedule-get");
    try {
        const timezone = await getUserTimezone(accountId, userId);
        console.error(`[schedule-get] formatted for timezone: ${timezone}`);
        const event = await scheduleGet(eventId, accountId);
        if (!event) {
            return { content: [{ type: "text", text: `Event not found: ${eventId}` }] };
        }
        const relLines = event.relationships.length > 0
            ? event.relationships
                .map((r) => `  -[${r.type}]-> ${r.targetLabel}: ${r.targetName ?? r.targetId}`)
                .join("\n")
            : "  (none)";
        const parts = [
            `Event: ${event.name}`,
            `ID: ${event.eventId}`,
            `Status: ${event.eventStatus}`,
            event.description ? `Description: ${event.description}` : null,
            `Starts: ${formatCombined(event.startDate, timezone)}`,
            event.endDate ? `Ends: ${formatCombined(event.endDate, timezone)}` : null,
            event.location ? `Location: ${event.location}` : null,
            event.recurrence ? `Recurrence: ${event.recurrence}` : null,
            event.timezone ? `Timezone: ${event.timezone}` : null,
            event.nextRun ? `Next run: ${formatCombined(event.nextRun, timezone)}` : null,
            event.lastTriggered ? `Last triggered: ${formatCombined(event.lastTriggered, timezone)}` : null,
            event.skipNext ? `Skip next: yes` : null,
            event.notes ? `\nNotes:\n${event.notes}` : null,
            ``,
            `Relationships:\n${relLines}`,
            ``,
            `Created: ${formatCombined(event.createdAt, timezone)} | Updated: ${formatCombined(event.updatedAt, timezone)}`,
        ].filter((line) => line !== null);
        return { content: [{ type: "text", text: parts.join("\n") }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Get failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-update
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-update", "Update an event's properties or append a note. Use skipNext=true on recurring events to skip the next occurrence without cancelling the series. Pass action to set/replace an automated action, or action=null to clear it.", {
    eventId: z.string().describe("Event ID (UUID)"),
    name: z.string().optional().describe("New event title"),
    description: z.string().optional().describe("New description"),
    startDate: z.string().optional().describe("New start date (ISO 8601)"),
    endDate: z.string().optional().describe("New end date (ISO 8601)"),
    location: z.string().optional().describe("New location"),
    eventStatus: eventStatusSchema,
    recurrence: z.string().optional().describe("New cron expression"),
    timezone: z.string().optional().describe("New IANA timezone"),
    skipNext: z.boolean().optional().describe("Skip the next occurrence of a recurring event"),
    note: z.string().optional().describe("Note appended to event history with timestamp"),
    action: z.object({
        plugin: z.string().describe("Plugin name that exposes the tool"),
        tool: z.string().describe("MCP tool name to call when the event fires"),
        args: z.record(z.unknown()).optional().describe("Arguments to pass to the tool call"),
    }).nullable().optional().describe("Set/replace the automated action, or pass null to clear it"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("schedule-update");
    try {
        await scheduleUpdate({ ...params, accountId });
        if (params.skipNext) {
            return { content: [{ type: "text", text: `Skipped next occurrence of event: ${params.eventId}` }] };
        }
        return { content: [{ type: "text", text: `Event updated: ${params.eventId}` }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Update failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-cancel
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-cancel", "Cancel a scheduled event. For recurring events, this cancels the entire series. Idempotent — cancelling an already-cancelled event is a no-op.", {
    eventId: z.string().describe("Event ID (UUID)"),
    note: z.string().optional().describe("Cancellation reason"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("schedule-cancel");
    try {
        await scheduleCancel({ ...params, accountId });
        return { content: [{ type: "text", text: `Event cancelled: ${params.eventId}` }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Cancel failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// time-resolve
// ---------------------------------------------------------------------------
eagerTool(server, "time-resolve", "Any activity involving time (date, timestamp, day of week, month, duration) routes through time-resolve before any other reasoning. Returns the user's locale timezone view of the input expression with a human-readable relative delta.", {
    expression: z.string().describe("ISO 8601 datetime string to convert (e.g. '2026-04-03T11:00:00.000Z')"),
}, async ({ expression }) => {
    if (!accountId)
        return refuseNoAccount("time-resolve");
    try {
        const timezone = await getUserTimezone(accountId, userId);
        const result = formatCombined(expression, timezone);
        console.error(`[time-resolve] ${expression} → ${result} (${timezone})`);
        return { content: [{ type: "text", text: result }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Time resolve failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-export-ics
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-export-ics", "Export one or more scheduled events as an .ics (iCalendar) file under the account's output/ics/ directory. Returns the file path; deliver that file to the operator (see the file-presentation skill for how delivery works on this surface). Recurring events with daily/weekly/monthly cron patterns produce RRULE; unmappable cron patterns produce individual VEVENTs per occurrence.", {
    eventIds: z.array(z.string()).min(1).describe("Array of event IDs (UUIDs) to export"),
}, async ({ eventIds }) => {
    if (!accountId)
        return refuseNoAccount("schedule-export-ics");
    try {
        const result = await scheduleExportIcs(eventIds, accountId);
        const parts = [
            `Exported ${result.eventCount} event(s) to: ${result.outputPath}`,
        ];
        if (result.rruleMapped > 0)
            parts.push(`${result.rruleMapped} recurring event(s) mapped to RRULE`);
        if (result.rruleFallback > 0)
            parts.push(`${result.rruleFallback} recurring event(s) expanded to individual occurrences`);
        if (result.notFound.length > 0)
            parts.push(`Not found: ${result.notFound.join(", ")}`);
        parts.push(`\nThe .ics is at ${result.outputPath}. Tell the operator that exact path and that it is in their Artefacts panel (downloadable), reachable on their file share if set up.`);
        return { content: [{ type: "text", text: parts.join("\n") }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Export failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-import-ics
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-import-ics", "Parse an uploaded .ics (iCalendar) file and return structured event data. Does NOT create events — present the extracted events to the user for confirmation, then use schedule-event to create the confirmed ones.", {
    filePath: z.string().describe("Absolute path to the .ics file (from an attachment upload)"),
}, async ({ filePath }) => {
    if (!accountId)
        return refuseNoAccount("schedule-import-ics");
    try {
        const result = await scheduleImportIcs(filePath, accountId);
        if (result.events.length === 0 && result.parseErrors.length > 0) {
            return {
                content: [{ type: "text", text: `Parse failed:\n${result.parseErrors.join("\n")}` }],
                isError: true,
            };
        }
        const lines = [`Parsed ${result.events.length} event(s) from ${result.filename}:\n`];
        for (const e of result.events) {
            const parts = [`  - ${e.summary}`];
            parts.push(`    Start: ${e.dtstart}`);
            if (e.dtend)
                parts.push(`    End: ${e.dtend}`);
            if (e.location)
                parts.push(`    Location: ${e.location}`);
            if (e.recurrence)
                parts.push(`    Recurrence: ${e.recurrence}`);
            if (e.description)
                parts.push(`    Description: ${e.description.substring(0, 200)}`);
            lines.push(parts.join("\n"));
        }
        if (result.parseErrors.length > 0) {
            lines.push(`\nWarnings:\n${result.parseErrors.join("\n")}`);
        }
        lines.push(`\nPresent these events to the user for confirmation. Use schedule-event to create the confirmed ones.`);
        return { content: [{ type: "text", text: lines.join("\n") }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Import failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// schedule-archive-ics
// ---------------------------------------------------------------------------
eagerTool(server, "schedule-archive-ics", "Ingest an .ics (iCalendar) archive into the graph as :Meeting nodes with :Person attendees. " +
    "Distinct from schedule-import-ics (which only previews events for the operator to recreate via schedule-event). " +
    "Use mode='dry-run' first to preview attendee resolution: how many emails already exist as :Person in this account, " +
    "how many would be cross-account-skipped (foreign accountId — edge will NOT be written), and how many new Person stubs " +
    "would be created. Use mode='commit' to apply. Each VEVENT becomes one :Meeting MERGEd on (accountId, uid); attendees " +
    "MERGE :Person by lowercased email and write (:Person)-[:ATTENDED]->(:Meeting) edges carrying role + partstat. " +
    "Re-ingest of the same file is idempotent.", {
    filePath: z.string().describe("Absolute path to the .ics file inside the account directory."),
    mode: z.enum(["dry-run", "commit"]).describe("dry-run returns attendee-resolution preview without writing; commit writes."),
}, async ({ filePath, mode }) => {
    if (!accountId)
        return refuseNoAccount("schedule-archive-ics");
    if (!userId) {
        return {
            content: [
                {
                    type: "text",
                    text: "schedule-archive-ics requires USER_ID at spawn — every Meeting anchors to the importing AdminUser.",
                },
            ],
            isError: true,
        };
    }
    try {
        const result = await scheduleArchiveIcs({
            filePath,
            mode,
            accountId,
            userId,
            ...(sessionId !== undefined ? { sessionId } : {}),
        });
        if (result.mode === "dry-run") {
            const lines = [
                `[dry-run] ${result.filename} — ${result.events} VEVENT(s) parsed, ${result.validMeetings} writable meeting(s).`,
            ];
            if (result.skippedNoUid > 0)
                lines.push(`  skippedNoUid: ${result.skippedNoUid}`);
            if (result.skippedNoStartTime > 0)
                lines.push(`  skippedNoStartTime: ${result.skippedNoStartTime}`);
            lines.push(`\nAttendee preview (would happen on commit):`);
            lines.push(`  attendees: ${result.preview.attendees}, organizers: ${result.preview.organizers}`);
            lines.push(`  non-mailto entries (rooms / resources / groups): ${result.preview.nonMailtoAttendees}`);
            lines.push(`  existing :Person in this account: ${result.preview.existingSameAccount}`);
            lines.push(`  existing :Person under foreign accountId (would be SKIPPED): ${result.preview.existingCrossAccount}`);
            lines.push(`  new :Person stubs to create: ${result.preview.willCreate}`);
            if (result.sampleAttendees.length > 0) {
                lines.push(`\nSample resolution (up to 20):`);
                for (const s of result.sampleAttendees) {
                    const dn = s.displayName ? ` (${s.displayName})` : "";
                    const exDn = s.existingDisplayName ? ` → matches ${s.existingDisplayName}` : "";
                    lines.push(`  [${s.status}] ${s.address}${dn}${exDn}`);
                }
            }
            if (result.parseErrors.length > 0) {
                lines.push(`\nParse warnings:`);
                for (const e of result.parseErrors)
                    lines.push(`  ${e}`);
            }
            lines.push(`\nIf this looks right, re-run with mode='commit' to write.`);
            return { content: [{ type: "text", text: lines.join("\n") }] };
        }
        const lines = [
            `[commit] ${result.filename} — ${result.events} VEVENT(s) parsed.`,
            `  meetings: ${result.meetingsCreated} created, ${result.meetingsUpdated} updated`,
            `  people: ${result.peopleCreated} created, ${result.existingPersonsLinked} existing linked`,
            `  attendees: ${result.attendees}, organizers: ${result.organizers}`,
        ];
        if (result.skippedCrossAccountPerson > 0) {
            lines.push(`  skippedCrossAccountPerson: ${result.skippedCrossAccountPerson} (foreign accountId — no edge written)`);
        }
        if (result.skippedNoUid > 0)
            lines.push(`  skippedNoUid: ${result.skippedNoUid}`);
        if (result.skippedNoStartTime > 0)
            lines.push(`  skippedNoStartTime: ${result.skippedNoStartTime}`);
        if (result.parseErrors.length > 0) {
            lines.push(`\nParse warnings:`);
            for (const e of result.parseErrors)
                lines.push(`  ${e}`);
        }
        lines.push(`\nimportId=${result.importId} ms=${result.ms}`);
        return { content: [{ type: "text", text: lines.join("\n") }] };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Archive failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
// ---------------------------------------------------------------------------
// Shutdown + start
// ---------------------------------------------------------------------------
process.on("SIGINT", async () => {
    await closeDriver();
    process.exit(0);
});
const transport = new StdioServerTransport();
await server.connect(transport);
//# sourceMappingURL=index.js.map