#!/usr/bin/env node
import readline from 'node:readline';

const baseUrl = (process.env.PREM_SECURITY_URL || 'http://127.0.0.1:8077').replace(/\/$/, '');
const apiToken = process.env.PREM_SECURITY_API_TOKEN || '';
const parsedBaseUrl = new URL(baseUrl);
if (!['https:', 'http:'].includes(parsedBaseUrl.protocol)) throw new Error('PREM_SECURITY_URL must use HTTP or HTTPS.');
if (parsedBaseUrl.protocol !== 'https:' && !['127.0.0.1', 'localhost', '::1'].includes(parsedBaseUrl.hostname)) throw new Error('PREM_SECURITY_URL must use HTTPS outside local development.');
if (!apiToken) throw new Error('PREM_SECURITY_API_TOKEN is required. Create one from Profile menu -> API & MCP access in Cyberscan.');

function queryString(values = {}) {
  const query = new URLSearchParams();
  for (const [key, value] of Object.entries(values)) {
    if (value !== undefined && value !== null && value !== '') query.set(key, String(value));
  }
  const serialized = query.toString();
  return serialized ? `?${serialized}` : '';
}

async function api(path, options = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    ...options,
    signal: options.signal || AbortSignal.timeout(60_000),
    headers: { accept: 'application/json', authorization: `Bearer ${apiToken}`, ...(options.body ? { 'content-type': 'application/json' } : {}), ...options.headers }
  });
  const type = response.headers.get('content-type') || '';
  const body = type.includes('json') ? await response.json() : await response.text();
  if (!response.ok) throw Object.assign(new Error(body?.error?.message || body?.error || `Prem Cyberscan API request failed (${response.status}).`), { status: response.status, code: body?.error?.code, details: body?.error?.details });
  return body;
}

const paging = {
  limit: { type: 'integer', minimum: 1, maximum: 100 },
  offset: { type: 'integer', minimum: 0 }
};
const feedbackTarget = { runId: { type: 'string' }, findingId: { type: 'string' } };
const feedbackKey = { type: 'string', minLength: 8, maxLength: 200, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,199}$' };
const tools = [
  { name: 'list_repositories', description: 'List and filter repositories already authorized in the Cyberscan workspace. Repository access can only be changed in the web application.', inputSchema: { type: 'object', properties: { visibility: { type: 'string', enum: ['public', 'private', 'internal'] }, account: { type: 'string' }, language: { type: 'string' }, q: { type: 'string' }, ...paging }, additionalProperties: false } },
  { name: 'start_scan', description: 'Start an isolated security scan for an authorized repository using a server-configured review mode.', inputSchema: { type: 'object', required: ['repositoryId', 'idempotencyKey'], properties: { repositoryId: { type: 'string' }, ref: { type: 'string' }, commit: { type: 'string', pattern: '^[0-9a-fA-F]{40}$' }, mode: { type: 'string', enum: ['fast', 'balanced', 'pro', 'max'] }, idempotencyKey: { type: 'string', minLength: 8, maxLength: 200 } }, additionalProperties: false } },
  { name: 'cancel_scan', description: 'Request cooperative cancellation. The worker stops dispatching new model passes, publishes its newest durable checkpoint, and only then becomes terminal.', inputSchema: { type: 'object', required: ['runId'], properties: { runId: { type: 'string' } }, additionalProperties: false } },
  { name: 'continue_scan', description: 'Continue a stopped result checkpoint or retry only incomplete review bundles. This never silently falls back to a full scan.', inputSchema: { type: 'object', required: ['runId', 'kind', 'idempotencyKey'], properties: { runId: { type: 'string' }, kind: { type: 'string', enum: ['resume_results', 'retry_incomplete_bundles'] }, idempotencyKey: { type: 'string', minLength: 8, maxLength: 200 } }, additionalProperties: false } },
  { name: 'list_runs', description: 'List and filter scan runs in the authenticated workspace.', inputSchema: { type: 'object', properties: { repositoryId: { type: 'string' }, status: { type: 'string' }, mode: { type: 'string', enum: ['fast', 'balanced', 'pro', 'max'] }, model: { type: 'string' }, q: { type: 'string' }, ...paging }, additionalProperties: false } },
  { name: 'get_run', description: 'Read one scan run and its findings.', inputSchema: { type: 'object', required: ['runId'], properties: { runId: { type: 'string' } }, additionalProperties: false } },
  { name: 'get_run_status', description: 'Poll the truthful stage and terminal status of a scan run without inventing percentage progress.', inputSchema: { type: 'object', required: ['runId'], properties: { runId: { type: 'string' } }, additionalProperties: false } },
  { name: 'get_run_events', description: 'Read the ordered worker, product, checkpoint, and reconciliation activity recorded for a scan run.', inputSchema: { type: 'object', required: ['runId'], properties: { runId: { type: 'string' }, after: { type: 'integer', minimum: 0, default: 0 }, limit: { type: 'integer', minimum: 1, maximum: 500, default: 100 } }, additionalProperties: false } },
  { name: 'list_findings', description: 'List and filter findings from accessible scan runs.', inputSchema: { type: 'object', properties: { runId: { type: 'string' }, repositoryId: { type: 'string' }, status: { type: 'string' }, severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'] }, confidence: { type: 'string', enum: ['high', 'medium', 'low'] }, q: { type: 'string' }, ...paging }, additionalProperties: false } },
  { name: 'get_statistics', description: 'Get workspace scan, finding, token, and cost totals. Missing usage remains distinct from zero.', inputSchema: { type: 'object', properties: { repositoryId: { type: 'string' }, mode: { type: 'string', enum: ['fast', 'balanced', 'pro', 'max'] }, model: { type: 'string' }, from: { type: 'string', format: 'date-time' }, to: { type: 'string', format: 'date-time' } }, additionalProperties: false } },
  { name: 'get_report', description: 'Get the structured audit report or portable Markdown report for a scan run.', inputSchema: { type: 'object', required: ['runId'], properties: { runId: { type: 'string' }, format: { type: 'string', enum: ['audit', 'markdown'], default: 'audit' } }, additionalProperties: false } },
  { name: 'get_credits', description: 'Read the workspace credit wallet: available and reserved balance, per-model rates and maximum reservations, and recent credit activity (top-ups, holds, releases, settled review usage).', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
  { name: 'get_feedback', description: 'Read owner feedback for an exact run, or one finding within it. Read the current state revision before editing. This does not access shared reviews.', inputSchema: { type: 'object', required: ['runId'], properties: { ...feedbackTarget, after: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, additionalProperties: false } },
  { name: 'comment_run', description: 'Append an owner run comment. Requires a token explicitly enabled for feedback writes. Record evidence and uncertainty; comments do not change scanner results.', inputSchema: { type: 'object', required: ['runId', 'comment', 'idempotencyKey'], properties: { runId: feedbackTarget.runId, comment: { type: 'string', minLength: 1, maxLength: 5000 }, idempotencyKey: feedbackKey }, additionalProperties: false } },
  { name: 'comment_finding', description: 'Append an owner comment to one exact run finding. Requires feedback write access.', inputSchema: { type: 'object', required: ['runId', 'findingId', 'comment', 'idempotencyKey'], properties: { ...feedbackTarget, comment: { type: 'string', minLength: 1, maxLength: 5000 }, idempotencyKey: feedbackKey }, additionalProperties: false } },
  { name: 'update_finding_feedback', description: 'Update typed owner feedback with the revision just read. Requires feedback write access and reasons for non-relevance, false positives, accepted risk or severity overrides. On conflict inspect currentState and reapply deliberately with a new key. Scanner severity and evidence never change.', inputSchema: { type: 'object', required: ['runId', 'findingId', 'patch', 'expectedRevision', 'idempotencyKey'], properties: { ...feedbackTarget, patch: { type: 'object', minProperties: 1, properties: { relevance: { type: 'string', enum: ['unreviewed', 'relevant', 'not_relevant', 'needs_context'] }, disposition: { type: 'string', enum: ['needs_review', 'confirmed', 'false_positive', 'accepted_risk', 'fixed'] }, suggestedSeverity: { type: ['string', 'null'], enum: [null, 'informational', 'low', 'medium', 'high', 'critical'] } }, additionalProperties: false }, expectedRevision: { type: 'integer', minimum: 0 }, reason: { type: 'string', maxLength: 1000 }, comment: { type: 'string', maxLength: 5000 }, idempotencyKey: feedbackKey }, additionalProperties: false } }
];

async function call(name, args) {
  if (['get_feedback', 'comment_run', 'comment_finding', 'update_finding_feedback'].includes(name)) {
    const { runId, findingId, idempotencyKey, ...input } = args;
    const route = `/api/v1/runs/${encodeURIComponent(runId)}/${findingId ? `findings/${encodeURIComponent(findingId)}/` : ''}feedback`;
    if (name === 'get_feedback') return api(`${route}${queryString(input)}`);
    if (name !== 'comment_run' && !findingId) throw new Error('findingId is required.');
    if (name === 'comment_run' && findingId) throw new Error('comment_run does not accept findingId.');
    return api(route, { method: name === 'update_finding_feedback' ? 'PATCH' : 'POST', headers: { 'idempotency-key': idempotencyKey }, body: JSON.stringify(input) });
  }
  if (name === 'list_repositories') return api(`/api/v1/repositories${queryString(args)}`);
  if (name === 'start_scan') {
    const { idempotencyKey, ...input } = args;
    return api('/api/v1/runs', { method: 'POST', headers: { 'idempotency-key': idempotencyKey }, body: JSON.stringify(input) });
  }
  if (name === 'cancel_scan') return api(`/api/v1/runs/${encodeURIComponent(args.runId)}/cancel`, { method: 'POST' });
  if (name === 'continue_scan') {
    const { runId, kind, idempotencyKey } = args;
    return api(`/api/v1/runs/${encodeURIComponent(runId)}/continuations`, { method: 'POST', headers: { 'idempotency-key': idempotencyKey }, body: JSON.stringify({ kind }) });
  }
  if (name === 'list_runs') return api(`/api/v1/runs${queryString(args)}`);
  if (name === 'get_run') return api(`/api/v1/runs/${encodeURIComponent(args.runId)}`);
  if (name === 'get_run_status') return api(`/api/v1/runs/${encodeURIComponent(args.runId)}/status`);
  if (name === 'get_run_events') return api(`/api/v1/runs/${encodeURIComponent(args.runId)}/events?after=${encodeURIComponent(args.after || 0)}&limit=${encodeURIComponent(args.limit || 100)}`);
  if (name === 'list_findings') return api(`/api/v1/findings${queryString(args)}`);
  if (name === 'get_statistics') return api(`/api/v1/statistics${queryString(args)}`);
  if (name === 'get_report') {
    const path = `/api/v1/runs/${encodeURIComponent(args.runId)}/report${args.format === 'markdown' ? '.md' : ''}`;
    return api(path, { headers: { accept: args.format === 'markdown' ? 'text/markdown' : 'application/json' } });
  }
  if (name === 'get_credits') return api('/api/v1/billing');
  throw new Error(`Unknown tool: ${name}`);
}

function send(payload) { process.stdout.write(`${JSON.stringify(payload)}\n`); }
const input = readline.createInterface({ input: process.stdin, terminal: false });
input.on('line', async (line) => {
  let request;
  try {
    request = JSON.parse(line);
    if (request.method === 'initialize') return send({ jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2025-03-26', capabilities: { tools: {} }, serverInfo: { name: 'prem-security', version: '0.3.0' } } });
    if (request.method === 'notifications/initialized') return;
    if (request.method === 'tools/list') return send({ jsonrpc: '2.0', id: request.id, result: { tools } });
    if (request.method === 'tools/call') {
      try {
        const result = await call(request.params.name, request.params.arguments || {});
        return send({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }], ...(typeof result === 'object' ? { structuredContent: result } : {}) } });
      } catch (error) {
        return send({ jsonrpc: '2.0', id: request.id, result: { isError: true, content: [{ type: 'text', text: error.message }], structuredContent: { error: { message: error.message, ...(error.status ? { status: error.status } : {}), ...(error.code ? { code: error.code } : {}), ...(error.details ? { details: error.details } : {}) } } } });
      }
    }
    send({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found' } });
  } catch (error) {
    send({ jsonrpc: '2.0', id: request?.id ?? null, error: { code: -32700, message: error.message } });
  }
});
