/**
 * Operations Tools for Studiograph Agent
 *
 * These tools allow the agent to manage deployment:
 * - Deploy to Railway
 * - Redeploy Railway service
 */
import { Type as T } from 'typebox';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { randomBytes } from 'crypto';
import { wrapToolResult } from './graph-tools.js';
import { provisionRequiredRailwaySecrets, } from '../../cli/railway-provisioning.js';
import { pinStartCommand, readOwnVersion } from '../../cli/railway-pin.js';
const RAILWAY_API = 'https://backboard.railway.app/graphql/v2';
async function railwayQuery(token, query, variables = {}) {
    const res = await fetch(RAILWAY_API, {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query, variables }),
    });
    const json = await res.json();
    if (json.errors?.length)
        throw new Error(json.errors.map((e) => e.message).join(', '));
    return json.data;
}
function loadUserConfig() {
    const configPath = join(homedir(), '.studiograph', 'user.json');
    if (!existsSync(configPath))
        return {};
    try {
        return JSON.parse(readFileSync(configPath, 'utf-8'));
    }
    catch {
        return {};
    }
}
/**
 * Create operations tools for the agent
 */
export function createOpsTools(_workspacePath, workspaceConfig) {
    return [
        {
            name: 'deploy_railway',
            label: 'Deploy to Railway',
            description: 'Deploy the Studiograph workspace to Railway (first-time setup). Creates a project, service, volume, sets env vars, and triggers a deploy. Reads tokens from ~/.studiograph/user.json automatically. Only ask the user for tokens if they are not already saved. For redeploying an existing service, use redeploy_railway instead.',
            parameters: T.Object({
                railwayToken: T.Optional(T.String({ description: 'Railway API token (reads from user config if omitted)' })),
                projectName: T.Optional(T.String({ description: 'Railway project name (default: team name)' })),
                anthropicApiKey: T.Optional(T.String({ description: 'Anthropic API key for the AI agent' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    // Resolve tokens from params or user config
                    const userConfig = loadUserConfig();
                    const railwayToken = params.railwayToken || process.env.RAILWAY_TOKEN || userConfig.railway_token;
                    if (!railwayToken) {
                        return wrapToolResult({ success: false, error: 'No Railway token found. Provide railwayToken parameter, set RAILWAY_TOKEN env var, or save railway_token in ~/.studiograph/user.json.' });
                    }
                    const { randomBytes } = await import('crypto');
                    const apiKey = randomBytes(24).toString('base64url');
                    const projectName = params.projectName
                        || (workspaceConfig.team_name ? `${workspaceConfig.team_name} — Studiograph` : 'Studiograph');
                    // Pin to the exact CLI version, prerelease or not. The pre-A7
                    // non-prerelease branch used unpinned `npx studiograph`, which
                    // resolves to whatever @latest is at Railway boot — the same
                    // version-drift footgun the CLI deploy path had.
                    // See cli-owner-audit §6.2 + codex review appendix.
                    const pkgVersion = readOwnVersion();
                    const startCommand = `cd /workspace && npx --yes studiograph@${pkgVersion} serve`;
                    const query = (q, variables = {}) => railwayQuery(railwayToken, q, variables);
                    // Create project
                    const projectData = await query(`
            mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { id name } }
          `, { input: { name: projectName } });
                    const projectId = projectData.projectCreate.id;
                    // Get environment
                    const envData = await query(`
            query($id: String!) { project(id: $id) { environments { edges { node { id name } } } } }
          `, { id: projectId });
                    const envId = envData.project.environments.edges[0].node.id;
                    // Create empty service
                    const serviceData = await query(`
            mutation($input: ServiceCreateInput!) { serviceCreate(input: $input) { id name } }
          `, { input: { projectId, name: 'studiograph' } });
                    const serviceId = serviceData.serviceCreate.id;
                    // Set start command via the shared pin helper. Throws on Railway
                    // API errors — failing fast keeps a half-built service from
                    // booting with no/wrong start command.
                    await pinStartCommand((q, v) => query(q, v ?? {}), { serviceId, environmentId: envId }, pkgVersion, { startCommand });
                    // Create volume
                    await query(`
            mutation($input: VolumeCreateInput!) { volumeCreate(input: $input) { id } }
          `, { input: { projectId, serviceId, mountPath: '/workspace' } });
                    // Set env vars
                    const envVars = {
                        PORT: '3001',
                        API_KEYS: apiKey,
                    };
                    if (params.anthropicApiKey)
                        envVars.ANTHROPIC_API_KEY = params.anthropicApiKey;
                    await query(`
            mutation($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }
          `, { input: { projectId, environmentId: envId, serviceId, variables: envVars } });
                    // H4 (Codex): Phase-2 bootstrap secrets must also land on the
                    // freshly-created service. The CLI deploy path (deploy.ts)
                    // calls provisionRequiredRailwaySecrets — the agent tool used
                    // to skip it, which would brick services created via the
                    // agent under Phase 4's hard-fail. Idempotent: if a value is
                    // already set we leave it, otherwise we generate a fresh one.
                    const provisionedBootstrap = await provisionRequiredRailwaySecrets((q, v) => query(q, v ?? {}), { projectId, serviceId, environmentId: envId });
                    // Generate domain
                    let domain = '';
                    try {
                        const d = await query(`
              mutation($input: ServiceDomainCreateInput!) { serviceDomainCreate(input: $input) { id domain } }
            `, { input: { serviceId, environmentId: envId } });
                        domain = d.serviceDomainCreate?.domain || '';
                    }
                    catch { /* non-fatal */ }
                    // Connect image (triggers deploy)
                    await query(`
            mutation($id: String!, $input: ServiceConnectInput!) { serviceConnect(id: $id, input: $input) { id } }
          `, { id: serviceId, input: { image: 'node:20' } });
                    return wrapToolResult({
                        success: true,
                        projectId,
                        projectName: projectData.projectCreate.name,
                        serviceId,
                        domain: domain ? `https://${domain}` : null,
                        dashboardUrl: `https://railway.com/project/${projectId}`,
                        apiKey,
                        startCommand,
                        provisionedBootstrap,
                    });
                }
                catch (err) {
                    return wrapToolResult({ error: err instanceof Error ? err.message : String(err) });
                }
            },
        },
        // Redeploy existing Railway service
        {
            name: 'redeploy_railway',
            label: 'Redeploy Railway',
            description: 'Trigger a redeploy of an existing Railway service. Reads Railway IDs from workspace config (.studiograph/workspace.json), falling back to ~/.studiograph/user.json. Updates the start command to the current package version before redeploying. Call this after workspace_push to update the deployed server with latest commits — pushing alone does NOT update Railway.',
            parameters: T.Object({}),
            execute: async (_toolCallId, _params) => {
                try {
                    const userConfig = loadUserConfig();
                    const token = process.env.RAILWAY_TOKEN || userConfig.railway_token;
                    if (!token) {
                        return wrapToolResult({ success: false, error: 'No Railway token found. Set railway_token in ~/.studiograph/user.json or RAILWAY_TOKEN env var.' });
                    }
                    // Read Railway IDs from workspace config only
                    const projectId = workspaceConfig.railway?.project_id;
                    let serviceId = workspaceConfig.railway?.service_id;
                    let environmentId = workspaceConfig.railway?.environment_id;
                    if (!projectId) {
                        return wrapToolResult({ success: false, error: 'No Railway project linked. Run `studiograph redeploy <railway-dashboard-url>` first or set railway_project_id in ~/.studiograph/user.json.' });
                    }
                    // Resolve service and environment from API if not saved
                    if (!serviceId || !environmentId) {
                        const data = await railwayQuery(token, `
              query($id: String!) {
                project(id: $id) {
                  services { edges { node { id name } } }
                  environments { edges { node { id name } } }
                }
              }
            `, { id: projectId });
                        if (!environmentId) {
                            environmentId = data.project.environments.edges[0]?.node.id;
                        }
                        if (!serviceId) {
                            const services = data.project.services.edges.map((e) => e.node);
                            if (services.length === 0) {
                                return wrapToolResult({ success: false, error: 'No services found in this Railway project.' });
                            }
                            serviceId = services[0].id;
                        }
                    }
                    if (!environmentId) {
                        return wrapToolResult({ success: false, error: 'Could not determine Railway environment.' });
                    }
                    if (!serviceId) {
                        return wrapToolResult({ success: false, error: 'Could not determine Railway service.' });
                    }
                    // Update start command via the shared pin helper. Fatal on
                    // failure — the pre-A7 path swallowed errors, which left Railway
                    // running the old broken start command while the tool reported
                    // success. See cli-owner-audit §6.2 + codex review appendix.
                    const pkgVersion = readOwnVersion();
                    let startCommand;
                    try {
                        startCommand = await pinStartCommand((q, v) => railwayQuery(token, q, v), { serviceId, environmentId }, pkgVersion);
                    }
                    catch (err) {
                        return wrapToolResult({
                            success: false,
                            error: `Could not pin Railway start command to studiograph@${pkgVersion}: ` +
                                (err instanceof Error ? err.message : String(err)) +
                                '. Aborting redeploy so the service keeps running its prior version.',
                        });
                    }
                    // Find latest deployment and redeploy
                    const deployData = await railwayQuery(token, `
            query($projectId: String!, $serviceId: String!, $environmentId: String!) {
              deployments(
                input: { projectId: $projectId, serviceId: $serviceId, environmentId: $environmentId }
                first: 1
              ) {
                edges { node { id status } }
              }
            }
          `, { projectId, serviceId, environmentId });
                    const latestDeploy = deployData.deployments.edges[0]?.node;
                    if (!latestDeploy) {
                        return wrapToolResult({ success: false, error: 'No previous deployment found. Run `studiograph deploy railway` first.' });
                    }
                    await railwayQuery(token, `
            mutation($id: String!) {
              deploymentRedeploy(id: $id) { id status }
            }
          `, { id: latestDeploy.id });
                    return wrapToolResult({
                        success: true,
                        message: 'Redeploy triggered',
                        startCommand,
                        dashboardUrl: `https://railway.com/project/${projectId}`,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : String(err) });
                }
            },
        },
        // Phase 2: provision STUDIOGRAPH_MASTER_KEY (and STUDIOGRAPH_JWT_SECRET
        // if missing) on the linked Railway service. Idempotent — if both are
        // already set, returns success with `provisioned: []`. Owner-gated: the
        // agent only runs for authenticated workspace owners.
        {
            name: 'provision_master_key',
            label: 'Provision Master Key',
            description: 'Generate and set STUDIOGRAPH_MASTER_KEY (and STUDIOGRAPH_JWT_SECRET if missing) on the linked Railway service via the API. Idempotent — does NOT overwrite existing values. Use this when a Railway service is missing the master key required for the encrypted secrets store. Returns the names of the variables that were generated and set.',
            parameters: T.Object({}),
            execute: async (_toolCallId, _params) => {
                try {
                    const userConfig = loadUserConfig();
                    const token = process.env.RAILWAY_TOKEN || userConfig.railway_token;
                    if (!token) {
                        return wrapToolResult({ success: false, error: 'No Railway token found. Set railway_token in ~/.studiograph/user.json or RAILWAY_TOKEN env var.' });
                    }
                    const projectId = workspaceConfig.railway?.project_id;
                    const serviceId = workspaceConfig.railway?.service_id;
                    const environmentId = workspaceConfig.railway?.environment_id;
                    if (!projectId || !serviceId || !environmentId) {
                        return wrapToolResult({ success: false, error: 'No Railway service linked to this workspace. Run `studiograph redeploy <railway-dashboard-url>` first.' });
                    }
                    const queryFn = (q, v) => railwayQuery(token, q, v ?? {});
                    const ctx = { projectId, serviceId, environmentId };
                    const provisioned = await provisionRequiredRailwaySecrets(queryFn, ctx);
                    return wrapToolResult({
                        success: true,
                        provisioned,
                        message: provisioned.length === 0
                            ? 'All required env vars already present.'
                            : `Set ${provisioned.length} new variable(s): ${provisioned.join(', ')}. Trigger a redeploy for them to take effect.`,
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : String(err) });
                }
            },
        },
        // Phase 2: rotate the JWT signing secret. Generates a new 32-byte
        // base64 secret, writes via the Railway API, and triggers a redeploy
        // (the new value is only picked up at boot). Owner-gated by the
        // calling context. Invalidates every extant session token — the user
        // must explicitly accept that consequence.
        {
            name: 'rotate_jwt_secret',
            label: 'Rotate JWT Secret',
            description: 'Generate a new STUDIOGRAPH_JWT_SECRET on the linked Railway service. WARNING: this invalidates every extant session token — every user (including the caller) will need to log in again after the redeploy. The new secret is generated client-side and POSTed via the Railway API; the old value is overwritten and is unrecoverable. Confirm with the user before invoking.',
            parameters: T.Object({}),
            execute: async (_toolCallId, _params) => {
                try {
                    const userConfig = loadUserConfig();
                    const token = process.env.RAILWAY_TOKEN || userConfig.railway_token;
                    if (!token) {
                        return wrapToolResult({ success: false, error: 'No Railway token found.' });
                    }
                    const projectId = workspaceConfig.railway?.project_id;
                    const serviceId = workspaceConfig.railway?.service_id;
                    const environmentId = workspaceConfig.railway?.environment_id;
                    if (!projectId || !serviceId || !environmentId) {
                        return wrapToolResult({ success: false, error: 'No Railway service linked.' });
                    }
                    const queryFn = (q, v) => railwayQuery(token, q, v ?? {});
                    const next = randomBytes(32).toString('base64');
                    await queryFn(`mutation($input: VariableCollectionUpsertInput!) {
              variableCollectionUpsert(input: $input)
            }`, {
                        input: {
                            projectId,
                            environmentId,
                            serviceId,
                            variables: { STUDIOGRAPH_JWT_SECRET: next },
                        },
                    });
                    return wrapToolResult({
                        success: true,
                        message: 'STUDIOGRAPH_JWT_SECRET rotated. Trigger a redeploy for it to take effect — every existing session will be invalidated at next boot.',
                    });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err instanceof Error ? err.message : String(err) });
                }
            },
        },
    ];
}
//# sourceMappingURL=ops-tools.js.map