/**
 * The PIG MCP server.
 *
 * PIG is a first-class application for agents as well as for people, and this
 * is that surface. Any MCP client connects — Claude Code, Codex, prime-agent,
 * or a Buzz workspace agent through its ACP bridge — so a team member works
 * from the terminal they already live in rather than being made to visit a web
 * app to log a call.
 *
 * Design rules, learned from tool surfaces that went wrong:
 *
 * **Keep it small.** Ten tools, each doing one thing. A sprawling tool list
 * degrades model performance more than it adds capability; anything genuinely
 * niche belongs behind `pig_search` or the HTTP API.
 *
 * **Every tool is an authenticated API call.** This process holds a PIG API
 * key and talks to the same HTTP API the browser uses. It has no database
 * credentials and no privileged path, so an agent can never reach further than
 * the person it acts for.
 *
 * **Return prose, not just JSON.** Results are formatted for a model to read
 * and quote back to a human. A wall of raw JSON forces the model to re-derive
 * meaning that the server already knows.
 */
import { DEMAND_STAGES, MOTION_KINDS } from '@pig/core';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

export interface PigMcpOptions {
  /** Base URL of the PIG API, e.g. https://primeintellectgrowth.com */
  baseUrl: string;
  /** A PIG API key (`pig_…`). Scope it to `read` unless writes are wanted. */
  apiKey: string;
  fetchImpl?: typeof fetch;
}

class PigApi {
  constructor(private readonly options: PigMcpOptions) {}

  private get fetchImpl() {
    return this.options.fetchImpl ?? fetch;
  }

  async request<T>(path: string, init: RequestInit = {}): Promise<T> {
    const response = await this.fetchImpl(`${this.options.baseUrl}${path}`, {
      ...init,
      headers: {
        authorization: `Bearer ${this.options.apiKey}`,
        'content-type': 'application/json',
        accept: 'application/json',
        ...(init.headers ?? {}),
      },
    });

    if (!response.ok) {
      const body = await response.text().catch(() => '');
      // Surface the real reason. An agent that is told "403 needs_profile" can
      // tell its user to get an invite; one told "request failed" cannot.
      throw new Error(
        `PIG API ${response.status}: ${body.slice(0, 300) || response.statusText}`,
      );
    }
    return (await response.json()) as T;
  }
}

const money = (cents: number | null | undefined, currency = 'USD') =>
  cents == null
    ? '—'
    : new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(cents / 100);

const pct = (v: number | null | undefined) =>
  v == null ? '—' : `${(v * 100).toFixed(1)}%`;

/** Wrap a handler so a thrown error becomes a readable tool error. */
function ok(text: string) {
  return { content: [{ type: 'text' as const, text }] };
}

export function createPigMcpServer(options: PigMcpOptions): McpServer {
  const api = new PigApi(options);
  const server = new McpServer({ name: 'pig', version: '0.1.0' });

  // ------------------------------------------------------------------ whoami

  server.registerTool(
    'pig_whoami',
    {
      title: 'Who am I in PIG',
      description:
        'Identify the PIG user this agent is acting for, and which teams they belong to ' +
        '(supply, demand, research). Call this first when you need to know whose pipeline ' +
        'to look at or whether the user may see supply-side economics.',
      inputSchema: {},
    },
    async () => {
      const me = await api.request<{
        name: string;
        email: string;
        isPlatformAdmin: boolean;
        teams: { team: string; role: string }[];
      }>('/api/me');
      const teams = me.teams.length
        ? me.teams.map((t) => `${t.team} (${t.role})`).join(', ')
        : 'no team membership';
      return ok(
        `${me.name} <${me.email}>\nTeams: ${teams}${me.isPlatformAdmin ? '\nPlatform admin.' : ''}`,
      );
    },
  );

  // -------------------------------------------------------------- my pipeline

  server.registerTool(
    'pig_my_pipeline',
    {
      title: 'My pipeline',
      description:
        'Summarise the current state of the business: margin across the capacity book, ' +
        'open deals on both sides, and any idle-capacity alerts. Good opening call when ' +
        'the user asks "where are we?" or "what needs attention?".',
      inputSchema: {},
    },
    async () => {
      const d = await api.request<{
        margin: {
          revenueCents: number;
          costCents: number;
          grossMarginCents: number;
          grossMarginPct: number | null;
          utilisation: number;
          idleGpuHours: number;
        };
        blocks: number;
        openDemandDeals: number;
        openSupplyDeals: number;
        idleAlerts: { name: string; gpuType: string; idleCostCents?: number }[];
      }>('/api/dashboard');

      const lines = [
        `Capacity book: ${d.blocks} commitment(s), ${pct(d.margin.utilisation)} utilised.`,
        `Revenue ${money(d.margin.revenueCents)} against cost ${money(d.margin.costCents)}.`,
        `Gross margin ${money(d.margin.grossMarginCents)} (${pct(d.margin.grossMarginPct)}).`,
        `Idle: ${Math.round(d.margin.idleGpuHours).toLocaleString()} GPU-hours bought and unsold.`,
        '',
        `Open deals — demand ${d.openDemandDeals}, supply ${d.openSupplyDeals}.`,
      ];

      if (d.idleAlerts.length) {
        lines.push('', 'Idle capacity worth attention:');
        for (const a of d.idleAlerts) {
          lines.push(`  • ${a.name} (${a.gpuType}) — ${money(a.idleCostCents)} unsold`);
        }
      }
      return ok(lines.join('\n'));
    },
  );

  // ------------------------------------------------------------ capacity match

  server.registerTool(
    'pig_capacity_match',
    {
      title: 'Match capacity to a requirement',
      description:
        'THE most useful tool here. Given what a customer needs — GPU type, count, dates, ' +
        'whether they need high-speed interconnect for distributed training — find capacity ' +
        'we have already committed to and could sell them, ranked by fit. Prefers blocks ' +
        'sitting idle, because those hours are already paid for. Warns when a match would ' +
        'sell below break-even.',
      inputSchema: {
        gpuCount: z.number().int().positive().describe('Number of GPUs required'),
        gpuType: z
          .string()
          .optional()
          .describe('Preferred GPU, e.g. H100_80GB, H200, B200. Omit to consider all.'),
        gpuTypeAlternatives: z
          .array(z.string())
          .optional()
          .describe('Acceptable substitutes, in preference order'),
        totalGpuHours: z.number().positive().optional().describe('Total GPU-hours needed'),
        requiresHighSpeedInterconnect: z
          .boolean()
          .optional()
          .describe(
            'True for distributed training. Excludes Ethernet-only and unverified fabric.',
          ),
        startsAt: z.string().optional().describe('ISO-8601 start date'),
        endsAt: z.string().optional().describe('ISO-8601 end date'),
        maxPricePerGpuHourCents: z
          .number()
          .int()
          .positive()
          .optional()
          .describe("Customer's price ceiling, in cents per GPU-hour"),
      },
    },
    async (input) => {
      const matches = await api.request<
        {
          name: string;
          gpuType: string;
          gpuCount: number;
          interconnectType: string;
          availableGpuHours: number;
          utilisation: number;
          costPerGpuHourCents: number;
          breakEvenPriceCents: number | null;
          score: number;
          rationale: string[];
        }[]
      >('/api/capacity/match', { method: 'POST', body: JSON.stringify(input) });

      if (matches.length === 0) {
        return ok(
          'No committed capacity matches that requirement.\n\n' +
            'Consider `pig_inventory_search` to find capacity available to buy from ' +
            'providers, which would need a new supply deal.',
        );
      }

      const lines = [`${matches.length} match(es), best first:\n`];
      for (const m of matches.slice(0, 8)) {
        lines.push(
          `▸ ${m.name} — ${m.gpuCount}× ${m.gpuType}, ${m.interconnectType}`,
          `  ${Math.round(m.availableGpuHours).toLocaleString()} GPU-hours free · ` +
            `${pct(m.utilisation)} utilised · cost ${money(m.costPerGpuHourCents)}/GPU-hr`,
          `  Break-even on remaining hours: ${money(m.breakEvenPriceCents)}/GPU-hr`,
          ...m.rationale.map((r) => `  ${r}`),
          '',
        );
      }
      return ok(lines.join('\n'));
    },
  );

  // ------------------------------------------------------------ margin report

  server.registerTool(
    'pig_margin_report',
    {
      title: 'Margin across the capacity book',
      description:
        'Per-commitment and total margin: what each block of capacity cost, what it earned, ' +
        'and how much of it is sold. Cost is charged against the FULL commitment, not only ' +
        'the hours that sold, because unsold hours are already paid for.',
      inputSchema: {},
    },
    async () => {
      const report = await api.request<{
        totals: {
          revenueCents: number;
          costCents: number;
          grossMarginCents: number;
          grossMarginPct: number | null;
          utilisation: number;
          idleGpuHours: number;
          marginPerAllocatedGpuHourCents: number | null;
        };
        blocks: {
          name: string;
          gpuType: string;
          utilisation: number;
          soldGpuHours: number;
          totalGpuHours: number;
          costPerGpuHourCents: number;
        }[];
      }>('/api/capacity/margin');

      const t = report.totals;
      const lines = [
        'Book totals',
        `  Revenue        ${money(t.revenueCents)}`,
        `  Cost           ${money(t.costCents)}`,
        `  Gross margin   ${money(t.grossMarginCents)} (${pct(t.grossMarginPct)})`,
        `  Per sold hour  ${money(t.marginPerAllocatedGpuHourCents)}`,
        `  Utilisation    ${pct(t.utilisation)}`,
        `  Idle           ${Math.round(t.idleGpuHours).toLocaleString()} GPU-hours`,
        '',
        'By commitment',
      ];
      for (const b of report.blocks) {
        lines.push(
          `  ${b.name} (${b.gpuType}) — ${pct(b.utilisation)} sold ` +
            `(${Math.round(b.soldGpuHours).toLocaleString()}/${Math.round(b.totalGpuHours).toLocaleString()} hrs) ` +
            `at ${money(b.costPerGpuHourCents)}/hr cost`,
        );
      }
      return ok(lines.join('\n'));
    },
  );

  // ------------------------------------------------------------ idle capacity

  server.registerTool(
    'pig_idle_capacity',
    {
      title: 'Idle capacity',
      description:
        'Committed capacity that is not sold, ranked by what it is costing. These hours are ' +
        'already paid for, so this is money leaving the business every hour it stays unsold. ' +
        'Use it to decide what to push.',
      inputSchema: {
        thresholdPct: z
          .number()
          .min(0)
          .max(1)
          .optional()
          .describe('Minimum idle fraction to report. Default 0.25.'),
        withinDays: z
          .number()
          .int()
          .positive()
          .optional()
          .describe('Only blocks live or starting within this many days. Default 30.'),
      },
    },
    async (input) => {
      const params = new URLSearchParams();
      if (input.thresholdPct != null) params.set('threshold', String(input.thresholdPct));
      if (input.withinDays != null) params.set('withinDays', String(input.withinDays));

      const rows = await api.request<
        {
          name: string;
          gpuType: string;
          gpuCount: number;
          idleGpuHours: number;
          idleCostCents: number;
          utilisation: number;
          breakEvenPriceCents: number | null;
          endsAt: string;
        }[]
      >(`/api/capacity/idle?${params}`);

      if (rows.length === 0) return ok('No idle capacity above the threshold. The book is tight.');

      const total = rows.reduce((s, r) => s + r.idleCostCents, 0);
      const lines = [`${rows.length} block(s) with idle capacity — ${money(total)} at stake:\n`];
      for (const r of rows) {
        lines.push(
          `▸ ${r.name} — ${r.gpuCount}× ${r.gpuType}`,
          `  ${Math.round(r.idleGpuHours).toLocaleString()} GPU-hours unsold, costing ${money(r.idleCostCents)}`,
          `  ${pct(r.utilisation)} utilised · sell above ${money(r.breakEvenPriceCents)}/GPU-hr to break even`,
          `  Block ends ${new Date(r.endsAt).toISOString().slice(0, 10)}`,
          '',
        );
      }
      return ok(lines.join('\n'));
    },
  );

  // -------------------------------------------------------- inventory search

  server.registerTool(
    'pig_inventory_search',
    {
      title: 'Search provider inventory',
      description:
        'Search GPU capacity available to BUY from providers — synced live from the compute ' +
        'marketplace. This is capacity we do not yet hold; use it when demand exists that ' +
        'committed capacity cannot cover, then open a supply deal.',
      inputSchema: {
        gpuType: z.string().optional().describe('e.g. H100_80GB, H200, B200'),
        minGpuCount: z.number().int().positive().optional(),
        maxPriceCents: z
          .number()
          .int()
          .positive()
          .optional()
          .describe('Ceiling on on-demand price, in cents per GPU-hour'),
        requiresHighSpeedInterconnect: z.boolean().optional(),
        limit: z.number().int().positive().max(100).optional(),
      },
    },
    async (input) => {
      const params = new URLSearchParams();
      if (input.gpuType) params.set('gpuType', input.gpuType);
      if (input.minGpuCount) params.set('minGpuCount', String(input.minGpuCount));
      if (input.maxPriceCents) params.set('maxPriceCents', String(input.maxPriceCents));
      if (input.requiresHighSpeedInterconnect) params.set('fastFabric', 'true');
      if (input.limit) params.set('limit', String(input.limit));

      const rows = await api.request<
        {
          providerSlug: string | null;
          gpuType: string;
          gpuCount: number;
          socket: string | null;
          interconnectType: string;
          region: string | null;
          country: string | null;
          securityTier: string;
          stockStatus: string;
          onDemandPriceCents: number | null;
          provisioningMinutes: number | null;
          observedAt: string;
        }[]
      >(`/api/inventory?${params}`);

      if (rows.length === 0) {
        return ok(
          'No matching inventory. Note that sync may be disabled, or the filters may be ' +
            'narrower than current market supply.',
        );
      }

      const lines = [`${rows.length} listing(s):\n`];
      for (const r of rows.slice(0, 25)) {
        lines.push(
          `▸ ${r.gpuCount}× ${r.gpuType}${r.socket ? ` ${r.socket}` : ''} — ` +
            `${money(r.onDemandPriceCents)}/GPU-hr`,
          `  ${r.providerSlug ?? 'unknown provider'} · ${r.region ?? r.country ?? 'region unknown'} · ` +
            `${r.interconnectType} · ${r.securityTier} · ${r.stockStatus}` +
            (r.provisioningMinutes ? ` · ~${r.provisioningMinutes}m to provision` : ''),
        );
      }
      lines.push(
        '',
        `Prices last observed ${new Date(rows[0]!.observedAt).toISOString().slice(0, 16)}Z.`,
      );
      return ok(lines.join('\n'));
    },
  );

  // ------------------------------------------------------------------ search

  server.registerTool(
    'pig_search',
    {
      title: 'Search accounts',
      description:
        'Find accounts by name. Returns which side of the market they sit on (supply, demand, ' +
        'or both) so you know whether they are a provider, a customer, or each in turn.',
      inputSchema: {
        query: z.string().min(1).describe('Name or partial name'),
        side: z.enum(['supply', 'demand', 'all']).optional(),
      },
    },
    async (input) => {
      const params = new URLSearchParams({ q: input.query });
      if (input.side) params.set('side', input.side);

      const rows = await api.request<
        {
          id: string;
          name: string;
          domain: string | null;
          side: string;
          supplierType: string | null;
          customerSegment: string | null;
          country: string | null;
          confidence: string;
        }[]
      >(`/api/accounts?${params}`);

      if (rows.length === 0) return ok(`No accounts matching "${input.query}".`);

      return ok(
        rows
          .slice(0, 25)
          .map(
            (r) =>
              `▸ ${r.name}${r.domain ? ` (${r.domain})` : ''} — ${r.side}` +
              `${r.supplierType ? `, ${r.supplierType}` : ''}` +
              `${r.customerSegment ? `, ${r.customerSegment}` : ''}` +
              `${r.confidence !== 'confirmed' ? ` [${r.confidence}]` : ''}\n  id: ${r.id}`,
          )
          .join('\n'),
      );
    },
  );

  // ------------------------------------------------------------- get account

  server.registerTool(
    'pig_get_account',
    {
      title: 'Get an account',
      description:
        'Full detail for one account: contacts, deals on both sides, contracts, and recent ' +
        'activity. Use `pig_search` first to find the id.',
      inputSchema: { accountId: z.string().uuid() },
    },
    async (input) => {
      const d = await api.request<{
        account: Record<string, unknown> & { name: string; side: string };
        contacts: { fullName: string; title: string | null; confidence: string }[];
        demandDeals: { name: string; stage: string; acvCents: number | null }[];
        supplyDeals: { name: string; stage: string }[];
        contracts: { title: string; type: string; status: string }[];
        activities: { type: string; subject: string | null; occurredAt: string }[];
      }>(`/api/accounts/${input.accountId}`);

      const lines = [`${d.account.name} — ${d.account.side} side`, ''];

      if (d.contacts.length) {
        lines.push('Contacts');
        for (const c of d.contacts) {
          lines.push(
            `  • ${c.fullName}${c.title ? ` — ${c.title}` : ''}` +
              // Surface weak provenance rather than presenting every record as
              // equally solid; some of these are single-source claims.
              (c.confidence !== 'confirmed' ? ` [${c.confidence}]` : ''),
          );
        }
        lines.push('');
      }

      if (d.demandDeals.length) {
        lines.push('Demand deals');
        for (const x of d.demandDeals) {
          lines.push(`  • ${x.name} — ${x.stage}${x.acvCents ? ` · ${money(x.acvCents)} ACV` : ''}`);
        }
        lines.push('');
      }

      if (d.supplyDeals.length) {
        lines.push('Supply deals');
        for (const x of d.supplyDeals) lines.push(`  • ${x.name} — ${x.stage}`);
        lines.push('');
      }

      if (d.contracts.length) {
        lines.push('Contracts');
        for (const x of d.contracts) {
          lines.push(`  • ${x.title} — ${x.type.toUpperCase()}, ${x.status}`);
        }
        lines.push('');
      }

      if (d.activities.length) {
        lines.push('Recent activity');
        for (const a of d.activities.slice(0, 8)) {
          lines.push(
            `  • ${new Date(a.occurredAt).toISOString().slice(0, 10)} ${a.type}` +
              `${a.subject ? ` — ${a.subject}` : ''}`,
          );
        }
      }

      return ok(lines.join('\n'));
    },
  );

  // ----------------------------------------------------------- motion library

  server.registerTool(
    'pig_motion_library',
    {
      title: 'Search the motion library',
      description:
        'Find reusable go-to-market practice: discovery guides, qualification frameworks, POC ' +
        'structures, proposal blocks, pricing inputs, reference architectures, case studies, ' +
        'technical narratives and deployment playbooks, each bound to the demand stage it ' +
        'serves. Use it before writing a proposal or scoping a POC from scratch — a template ' +
        'with a usage count is language that has already survived a customer.',
      inputSchema: {
        kind: z
          .enum(MOTION_KINDS)
          .optional()
          .describe('Restrict to one kind of template. Omit for all nine.'),
        stage: z
          .enum(DEMAND_STAGES)
          .optional()
          .describe('Restrict to templates serving one demand stage, e.g. proposal.'),
        query: z.string().optional().describe('Matched against title, summary and slug'),
      },
    },
    async (input) => {
      const params = new URLSearchParams();
      if (input.kind) params.set('kind', input.kind);
      if (input.stage) params.set('stage', input.stage);
      if (input.query) params.set('q', input.query);

      const { templates, truncated } = await api.request<{
        templates: {
          id: string;
          kind: string;
          slug: string;
          version: number;
          title: string;
          summary: string;
          stage: string;
          visibility: string;
          usageCount: number;
          updatedAt: string;
        }[];
        truncated: boolean;
      }>(`/api/motion/templates?${params}`);

      if (templates.length === 0) {
        return ok(
          'No template matches that request. Note that this key sees shared templates and ' +
            "the caller's own private drafts, so a colleague's draft will not appear.",
        );
      }

      const lines = [
        `${templates.length}${truncated ? '+' : ''} template(s), newest version of each:\n`,
      ];
      for (const t of templates.slice(0, 25)) {
        lines.push(
          `▸ ${t.title} — ${t.kind}, ${t.stage} stage, v${t.version}` +
            `${t.visibility === 'private' ? ' [private draft]' : ''}`,
          `  ${t.summary}`,
          // Usage is the difference between practice and a document nobody
          // opened, and it is the field a model should rank on.
          `  Instantiated ${t.usageCount} time(s) · id: ${t.id}`,
          '',
        );
      }
      return ok(lines.join('\n'));
    },
  );

  // ------------------------------------------------------------- log activity

  server.registerTool(
    'pig_log_activity',
    {
      title: 'Log an activity',
      description:
        'Record a call, meeting, email or note against an account. Use this after a ' +
        'conversation so the CRM reflects what actually happened rather than what someone ' +
        'remembers to type in later.',
      inputSchema: {
        accountId: z.string().uuid(),
        type: z.enum(['note', 'email', 'call', 'meeting', 'slack', 'buzz']),
        subject: z.string().min(1).max(200),
        body: z.string().max(8000).optional(),
        occurredAt: z.string().optional().describe('ISO-8601. Defaults to now.'),
      },
    },
    async (input) => {
      await api.request('/api/activities', {
        method: 'POST',
        body: JSON.stringify(input),
      });
      return ok(`Logged: ${input.type} — ${input.subject}`);
    },
  );

  return server;
}
