/**
 * Which capability each read requires — the whole policy, in one table.
 *
 * It lives in a table rather than beside each handler because the question a
 * reviewer needs to answer is "who can see cost?", and that question is
 * unanswerable if the answer is spread across nine route files. Adding a GET
 * without adding a row here leaves it ungoverned, which is the failure this
 * exists to end; `read-governance.test.ts` fails when a new read path appears
 * that no row covers.
 *
 * Mounted before every other route in `createApp`, and the order is
 * load-bearing: Hono runs matched handlers in registration order, so a guard
 * registered after its handler never runs.
 */
import type {
  PiggyChatContext,
  PiggyPageRoute,
  PiggyRecordType,
  ReadCapability,
} from '@pig/core';
import { Hono } from 'hono';
import { readGuard } from '../lib/read-guard';
import type { ApiEnv } from '../lib/mutation';
import { PIGGY_ACTIVITY_PATH } from './piggy-activity';

export interface ReadRule {
  method: 'GET' | 'POST';
  path: string;
  capability: ReadCapability;
}

/** Spelled once: the row below and the relay's own check must never diverge. */
export const PIGGY_CHAT_PATH = '/api/piggy/chat';

/**
 * `economics:read` covers anything carrying supplier cost, break-even price or
 * a margin total. `/api/capacity/match` is a POST only because a requirement
 * is too big for a query string — it returns break-even per block, so it is a
 * read and is gated as one.
 */
export const READ_RULES: readonly ReadRule[] = [
  { method: 'GET', path: '/api/capacity/availability', capability: 'economics:read' },
  { method: 'GET', path: '/api/capacity/idle', capability: 'economics:read' },
  { method: 'GET', path: '/api/capacity/margin', capability: 'economics:read' },
  { method: 'POST', path: '/api/capacity/match', capability: 'economics:read' },
  { method: 'GET', path: '/api/inventory', capability: 'economics:read' },
  { method: 'GET', path: '/api/commitments', capability: 'economics:read' },
  { method: 'GET', path: '/api/allocations', capability: 'economics:read' },
  { method: 'GET', path: '/api/dashboard', capability: 'economics:read' },

  { method: 'GET', path: '/api/accounts', capability: 'book:read' },
  { method: 'GET', path: '/api/accounts/:id', capability: 'book:read' },
  { method: 'GET', path: '/api/contacts', capability: 'book:read' },
  { method: 'GET', path: '/api/deals/demand', capability: 'book:read' },
  { method: 'GET', path: '/api/deals/supply', capability: 'book:read' },
  { method: 'GET', path: '/api/contracts', capability: 'book:read' },
  { method: 'GET', path: '/api/contracts/:id', capability: 'book:read' },
  { method: 'GET', path: '/api/growth', capability: 'book:read' },
  { method: 'GET', path: '/api/growth/accounts/:id', capability: 'book:read' },
  { method: 'GET', path: '/api/facts', capability: 'book:read' },

  /**
   * The motion library is not the cost book. A proposal block, a reference
   * architecture and a qualification framework carry no supplier cost and no
   * break-even, so gating them on `economics:read` would keep a research lead
   * out of the reference architectures they are the ones writing.
   *
   * The private/shared split is NOT enforced here and could not be: this table
   * is keyed on a path and knows nothing about rows. It is a real WHERE clause
   * in `services/motion.ts`, which every one of these reads goes through.
   */
  { method: 'GET', path: '/api/motion', capability: 'book:read' },
  { method: 'GET', path: '/api/motion/templates', capability: 'book:read' },
  { method: 'GET', path: '/api/motion/templates/:id', capability: 'book:read' },
  { method: 'GET', path: '/api/motion/engagements', capability: 'book:read' },
  { method: 'GET', path: '/api/motion/engagements/:id', capability: 'book:read' },

  { method: 'GET', path: '/api/team', capability: 'team:read' },

  /**
   * The assistant reads the book on your behalf, so it is a read.
   *
   * `book:read` is the FLOOR, not the whole answer: what a turn may reach is
   * decided by the context in the body, which a path-keyed table cannot see.
   * `piggyContextCapability` below is the rest of the policy and the relay
   * applies it after parsing. The row still earns its place — it puts the chat
   * POST under the same generic denials as every other read (no team, a
   * write-only credential) and under read-governance.test.ts with them.
   */
  { method: 'POST', path: PIGGY_CHAT_PATH, capability: 'book:read' },

  /**
   * A stored transcript is a read of the book by another name, so it is
   * governed like one — and like the chat POST, `book:read` is the FLOOR. What
   * a particular conversation was told is a property of the row, which this
   * table cannot see; `piggy_conversations.read_capability` carries it and
   * `PiggyConversationService.detail` enforces it. The row here is what denies
   * the stranger, the write-only credential and the person on no team.
   */
  { method: 'GET', path: '/api/piggy/conversations', capability: 'book:read' },
  { method: 'GET', path: '/api/piggy/conversations/:id', capability: 'book:read' },

  /**
   * The agent ledger — what Piggy ran, what is queued, what it cost.
   *
   * `book:read` although it returns money, because the money is what PIG spent
   * on inference, never supplier cost or margin. Gating it as economics would
   * mean a research lead could not see what their own questions cost, which is
   * an audit surface auditable only by the people who need it least. Who sees
   * whose runs is decided in `PiggyActivityService` by an ownership predicate:
   * your own, unless you are a platform admin, who sees the workspace.
   */
  { method: 'GET', path: PIGGY_ACTIVITY_PATH, capability: 'book:read' },
];

/**
 * Which capability a Piggy turn requires, decided by what its context reads.
 *
 * The hole this closes: the relay used to check the `read` SCOPE and nothing
 * else, so a viewer correctly 403'd on `GET /api/capacity/margin` could open
 * the dock on /margin and have `pig_get_margin_summary` read back book
 * revenue, supplier cost and break-even. Scope is a property of the
 * credential; this is the property of the person, and it has to be checked in
 * the same request.
 *
 * The classification is "what does this context's grounding tool return",
 * never "what does the page look like". `/accounts` sits in the economics
 * column because its tool is `pig_get_workspace_summary`, which returns book
 * revenue, cost and gross margin — gating it on `book:read` would hand the
 * cost book to anyone willing to ask about accounts instead of margin. The
 * same reasoning puts `commitment`, `supply_deal` and `demand_deal` there:
 * their reads reach `capacity_commitments` and `allocations`, which
 * `/api/commitments` and `/api/allocations` already gate as economics.
 *
 * If that feels too wide for /accounts or /learn, the fix is in
 * `apps/piggy/src/page-routes.ts` — give those pages a summary tool that
 * carries no cost — not a looser row here.
 *
 * Both tables are exhaustive on purpose. A context added to @pig/core without
 * a capability is a door nobody classified, and the compiler refusing it is
 * cheaper than discovering it in an audit.
 */
const PIGGY_PAGE_CAPABILITIES: Readonly<Record<PiggyPageRoute, ReadCapability>> = {
  // pig_get_workspace_summary — book revenue, cost, gross margin, worst idle.
  '/': 'economics:read',
  '/accounts': 'economics:read',
  '/imports': 'economics:read',
  '/team': 'economics:read',
  '/facts': 'economics:read',
  '/learn': 'economics:read',
  '/settings': 'economics:read',
  '/piggy': 'economics:read',
  // pig_get_margin_summary / pig_get_idle_capacity — cost and break-even.
  '/margin': 'economics:read',
  '/capacity': 'economics:read',
  // pig_get_pipeline and pig_get_calendar_ahead: deal values and dates, which
  // is the book every member already reads.
  '/growth': 'book:read',
  '/demand': 'book:read',
  '/supply': 'book:read',
  '/calendar': 'book:read',
  '/contracts': 'book:read',
  // The motion library is not the cost book. pig_get_motion_summary,
  // pig_search_motion_library and pig_get_engagement return templates,
  // artifacts and qualification scores — authored practice, carrying no
  // supplier cost and no break-even — so the classification follows what those
  // tools return rather than what the pages look like. The private/shared rule
  // is a separate matter and is enforced in the query, not here: the library
  // tool filters to `visibility = 'shared'` unconditionally.
  '/motion': 'book:read',
  '/motion/library': 'book:read',
  '/motion/engagements': 'book:read',
};

const PIGGY_RECORD_CAPABILITIES: Readonly<Record<PiggyRecordType, ReadCapability>> = {
  account: 'book:read',
  contact: 'book:read',
  contract: 'book:read',
  demand_deal: 'economics:read',
  supply_deal: 'economics:read',
  commitment: 'economics:read',
};

export function piggyContextCapability(context: PiggyChatContext | undefined): ReadCapability {
  // No context is the dashboard by another name — `createInteractivePigTools`
  // maps it to '/' — so it must not be the cheap way past the margin gate.
  if (!context) return PIGGY_PAGE_CAPABILITIES['/'];
  return context.type === 'page'
    ? PIGGY_PAGE_CAPABILITIES[context.route]
    : PIGGY_RECORD_CAPABILITIES[context.type];
}

export function createReadGuardRoutes(rules: readonly ReadRule[] = READ_RULES): Hono<ApiEnv> {
  const routes = new Hono<ApiEnv>();
  for (const rule of rules) routes.on(rule.method, rule.path, readGuard(rule.capability));
  return routes;
}
