/**
 * MGM GUI contract v0 (experimental): standalone extract for review and type-checking.
 *
 *   Part A  →  src/lib/gui/contract.ts   client-safe types and two constants. All a GUI author reads.
 *   Part B  →  src/lib/gui/surface.ts    server-only ("server-only" import there). What a capability
 *                                        plugin contributes to the root `mgmSurface` service.
 *
 * The kit runtime (src/lib/gui/kit, src/lib/gui/host) is declared here as `GuiKit` and implemented with
 * `satisfies GuiKit`, so the kit's shape moves under the same semver as the types.
 *
 * Nothing here is a rule the server trusts. Role, gate, release, RLS, the confirmation levels and the
 * close digest are enforced by the host, the routes and the database; these types make the right thing
 * the easy thing, and `contribute()` re-checks every rule at boot (see Part B).
 */
import type { ComponentType, ReactNode } from "react";
import type { Context } from "cordis";
import type { ZodType } from "zod";
import type { SupabaseClient, User } from "@supabase/supabase-js";
import type { MgmPluginName } from "@/lib/plugins/bundled";
import type { UserRoleRecord } from "@/lib/types/domain";

// ════════════════════════════════════════════════════════════════════════════════════════════════
// Part A: src/lib/gui/contract.ts (client-safe)
// ════════════════════════════════════════════════════════════════════════════════════════════════

/** Contract version (semver). A GUI declares the range it supports, e.g. "^0.1". */
export const GUI_CONTRACT = "0.1.0";

// ── Vocabulary ──────────────────────────────────────────────────────────────────────────────────

/** "<alias>.<name>": the alias is the contributing plugin's catalog alias ("billing", "org", "community"). */
export type Id = `${string}.${string}`;
/** A same-origin relative path ("/…", never "//…", no scheme). The host's parse rejects anything else. */
export type Href = `/${string}`;
/** The only external targets a view may carry: a WhatsApp chat, a call, an SMS. Nothing else leaves the origin. */
export type OutHref = `https://wa.me/${string}` | `tel:${string}` | `sms:${string}`;
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
/** Meaning, not colour. `accent` is the old "brand" tone (a label, never a state). */
export type Tone = "neutral" | "info" | "success" | "warning" | "danger" | "accent";
/** What a person does before a write. The host runs every level; a GUI only frames it (GuiPlugin.Overlay). */
export type Confirm = "none" | "confirm" | "typed" | "ceremony";
export type NavGroup = "operate" | "money" | "configure" | "system";
/** A fixed icon vocabulary, so every GUI can map names to its own set. Unknown names render as text only. */
export type Icon =
  | "today" | "live" | "sites" | "households" | "devices" | "readings" | "billing" | "payments"
  | "tariff" | "data-source" | "branding" | "accounts" | "plugins" | "users" | "profile"
  | "organizations" | "assistant" | "message" | "alert" | "download" | "share" | "contact" | "help";
export type CoreEntityKind =
  | "organization" | "community" | "microgrid" | "edge" | "device" | "household"
  | "rateSchedule" | "billingPeriod" | "lineItem" | "paymentAttempt";
/** Core kinds, or a plugin's own namespaced kind ("assistant.thread", "sms-reminders.send"). */
export type EntityKind = CoreEntityKind | `${string}.${string}`;
export interface EntityRef { kind: EntityKind; id: string; label: string; href?: Href }
export type EntityHit = EntityRef & { href: Href };
export type Trail = { label: string; href: Href }[];

// ── Values: formatted on the server, so a GUI may simply print `text` ────────────────────────────

type ValueMeta = {
  text: string;        // locale-, currency- and zone-formatted; in a bare column, the bare figure "4,216,800"
  derived?: true;      // computed rather than measured: mark it (≈, dashed)
  at?: string;         // ISO time measured; age it against Manifest.now, hatch it past staleAfterSeconds
  synthetic?: true;    // from the fixture metering provider: always labelled
};
export type Value = ValueMeta & (
  | { t: "text" } | { t: "empty" }
  | { t: "number"; v: number; unit?: string }
  | { t: "percent"; v: number }
  | { t: "money"; v: number; currency: string }      // v: integer minor units per ISO 4217 (UGX has none: shillings)
  | { t: "energy"; v: number; unit: "Wh" | "kWh" }
  | { t: "power"; v: number; unit: "W" | "kW" }
  | { t: "date"; v: string }                         // YYYY-MM-DD in the entity's zone
  | { t: "datetime"; v: string; tz: string }
  | { t: "status"; v: string; tone: Tone }           // tone from the shared status maps in fmt.ts
  | { t: "ref"; ref: EntityRef }
);
export type ValueType = Value["t"];

/** Server-built rich text: paragraphs and list items. De-pseudonymised and link-checked before it leaves the server. */
export type Inline =
  | string
  | { value: Value }                 // a grounded figure (from a loader's Value)
  | { link: Href; text: string }     // a released route, checked on the server
  | { unverified: string };          // a figure that matched no Value: render with an "unverified" mark
export type Rich = ({ p: Inline[] } | { li: Inline[] })[];

/** A GET the person opens, downloads or shares. Never a write; no confirmation. */
export interface LinkRef {
  id: Id; label: string; icon?: Icon;
  href: Href | OutHref;
  download?: { mime: string; filename: string };   // e.g. the bill PDF, the URA CSV
  share?: { title: string; text: string };         // a GUI may offer Web Share (files fetched ahead when canShare)
}

// ── Actions, as a GUI sees them ─────────────────────────────────────────────────────────────────

/** A control, resolved by the host for this person and entity. A GUI renders it and calls `kit.act(ref)`. */
export interface ActionRef {
  action: Id;
  params: Record<string, string>;  // route segments and bound body fields, filled by the host from the entity
  label: string;
  enabled: boolean; reason?: string;  // from role, gate, release and the action's `available`
  tone?: "danger";                    // typed or ceremony, delete or destructive
  from?: string;                      // set when another plugin contributed this control: "SMS reminders"
  prefill?: Json;                     // parsed against the input schema, writeOnly fields removed
  provenance?: { by: "assistant" | "watcher"; proposal?: string; at: string };  // present: render as proposed (dashed)
}
/** What a bespoke form needs. Fetched by the kit when the form opens, never shipped in the manifest. */
export interface ActionSpec {
  type: "spec"; id: Id; title: string; confirm: Confirm;
  inputVersion: number;   // a form declares the versions it reads; on a mismatch the kit uses its ActionForm
  input: Json;            // JSON Schema of the person-entered fields: bound fields removed, secrets marked writeOnly
}
export type ActionResult =
  | { type: "result"; ok: true; message?: string; href?: Href; next?: ActionRef; signals?: true }
  | { type: "result"; ok: false; status: number; code: string; message: string; field?: string };

// ── The five view kinds (generic over the action type: ActionRef for GUIs, ActionLink for loaders) ──

export interface Column { key: string; label: string; type: ValueType; priority: 1 | 2 | 3; copy?: "bare" }
export interface RowOf<A> {
  id: string; entity?: EntityRef; related?: EntityRef[]; href?: Href;
  cells: Record<string, Value>; actions: A[]; links?: LinkRef[];
}
export interface Filter { label: string; href: Href; count?: number; current: boolean }
export type AuthorKind = "person" | "assistant" | "system";
export type EmbedOf<A, V> =
  | ({ type: "view"; href: Href } & V)   // loaders give the href; the host loads, checks and parses the model
  | { type: "action"; ref: A }
  | { type: "values"; values: Value[] }
  | { type: "steps"; steps: { label: string; ms: number; ok: boolean }[] };
export interface FeedEntryOf<A, V> {
  id: string; at: string; author: { kind: AuthorKind; name: string }; text: Rich; embed?: EmbedOf<A, V>;
}
/** A free-text input bound to an action (the assistant's Ask). The kit sends text and the current view. */
export interface ComposeOf<A> { action: A; textField: string; contextField?: string; placeholder: string }
export interface BodiesOf<A, V> {
  collection: {
    columns: Column[]; rows: RowOf<A>[];
    totals?: Record<string, Value>;      // rendered below the rows, never as a row
    filters?: Filter[]; next?: Href;     // bulk selection is out of scope for v0
    empty: { title: string; text?: string; action?: A };  // at zero rows the empty state owns the create action
  };
  record: { sections: { title?: string; fields: { key: string; label: string; value: Value }[] }[] };
  metrics: {
    tiles: { key: string; label: string; value: Value; status?: Value; hint?: string }[];
    series?: { key: string; label: string; unit: string; points: [string, number | null][] }[];  // null is a gap
  };
  feed: { entries: FeedEntryOf<A, V>[]; compose?: ComposeOf<A>; stop?: A; busy?: boolean };
  flow: {
    steps: { id: string; title: string; state: "done" | "current" | "todo" | "blocked";
             detail?: string; href?: Href; action?: A }[];
  };
}
export type ViewKind = keyof BodiesOf<unknown, unknown>;

export interface ModelBase<K extends ViewKind, A, V> {
  type: "view"; view: Id; kind: K; body: BodiesOf<A, V>[K];
  title: string; subtitle?: string;
  org: string | null; entity?: EntityRef; trail: Trail;
  actions: A[]; links: LinkRef[];
  attached: { view: Id; title: string; href: Href; hint: "section" | "tab" | "tile" }[];  // the GUI decides
  notice?: { tone: Tone; text: string };                      // set by the host, e.g. from the gate's state
  live?: { refreshSeconds: number; staleAfterSeconds: number };
  ext?: { schema: `${Id}@${number}`; data: Json };           // only for a renderer that reads this version
}
export interface EmbeddedView { model: ViewModel }
export type ModelOf<K extends ViewKind> = ModelBase<K, ActionRef, EmbeddedView>;
export type ViewModel = { [K in ViewKind]: ModelOf<K> }[ViewKind];
/** The contract version each kind arrived in; the host gives a GUI only kinds its declared range covers. */
export const GUI_KINDS_SINCE: Record<ViewKind, string> = {
  collection: "0.1.0", record: "0.1.0", metrics: "0.1.0", feed: "0.1.0", flow: "0.1.0",
};

// ── What the Shell receives ─────────────────────────────────────────────────────────────────────

type SignalBase = {
  id: string; source: Id; severity: "info" | "warn" | "critical"; title: string;
  facts?: Value[]; entity?: EntityRef; href: Href; at: string; due?: string;
};
export type SignalOf<A> =
  | (SignalBase & { kind: "alarm" })
  | (SignalBase & { kind: "your-move" })                 // never carries an action: it links to its step
  | (SignalBase & { kind: "proposal"; action: A })       // the action carries provenance: render dashed
  | (SignalBase & { kind: "question" })                  // answered where `href` points (its thread)
  | (SignalBase & { kind: "info" });
export type Signal = SignalOf<ActionRef>;
/** Streamed after the first paint (GET /api/gui/signals), never part of the manifest. */
export interface Signals { type: "signals"; org: string; count: number; top: Signal[]; unchecked: Id[] }

export interface StatusItemOf<A> {
  id: Id; label: string; state: "off" | "idle" | "working" | "needs-you";
  open: Href;              // a feed or page to dock or link
  ask?: ComposeOf<A>;      // offered as "Ask" in a command palette
}
export type StatusItem = StatusItemOf<ActionRef>;
export interface NavEntry {
  id: Id; title: string; icon: Icon; group: NavGroup; order: number;
  href: Href;              // a pattern; the host fills [segments] by kind through ViewState.href
  needs: CoreEntityKind[]; // context the pattern needs; without it a GUI offers ViewState.choices
  legacy?: true;           // migration period: a released page with no contributed view yet
}
export interface OrgManifest {
  id: string; name: string; href: Href; timezone: string | null;
  nav: NavEntry[]; status: StatusItem[];
  places: EntityRef[];     // the organization's communities and microgrids, for context pickers (capped)
}
/** Built once per hard load for the current organization only. Cache-Control: no-store. */
export interface Manifest {
  type: "manifest"; contract: string; release: string; locale: string;
  now: string;             // server time, so ages are right on phones with wrong clocks
  gui: string;
  user: { name: string | null; email: string };
  org: OrgManifest;
  orgs: { id: string; name: string; href: Href }[];  // the switcher; other organizations load on switch
}
/** Resolved by the host from the request path, so the first server render is already right. */
export interface InitialView {
  view: Id | null; title: string; trail: Trail;
  tabs: { title: string; href: Href; current: boolean }[];
  scope: Partial<Record<CoreEntityKind, EntityRef>>;
}
export interface ViewState extends InitialView {
  model: ViewModel | null;                    // null on a legacy page
  org: OrgManifest;
  /** Fills the pattern by kind from scope (or `pick`); null when a needed kind is missing. */
  href(nav: NavEntry, pick?: Partial<Record<CoreEntityKind, EntityRef>>): Href | null;
  choices(kind: CoreEntityKind): EntityRef[]; // candidates for a context picker
}

// ── The kit: the only I/O a GUI has ─────────────────────────────────────────────────────────────

export interface LiveState {
  model: ViewModel;
  ageSeconds(value: Value): number | null;
  stale(value: Value): boolean;
  polling: "on" | "slow" | "paused";   // slow on Save-Data, effectiveType 2g/3g or saveData; paused when hidden
}
export interface FeedState {
  model: ModelOf<"feed"> | null; busy: boolean;
  send(text: string): Promise<ActionResult>;  // runs compose, then follows ActionResult.next until done or stopped
  stop(): void;
}
export interface LinkProps { href: Href; children: ReactNode; className?: string; current?: boolean }
export interface GuiKit {
  useManifest(): Manifest;
  useView(): ViewState;
  useSignals(): Signals | null;
  useLive(model: ViewModel): LiveState;
  useFeed(href: Href): FeedState;
  loadView(href: Href): Promise<ViewModel | ActionResult>;
  search(query: string, prefix: "@" | "#"): Promise<EntityHit[]>;
  Link: ComponentType<LinkProps>;
  navigate(href: Href): void;
  refresh(): void;
  usePathname(): string;
  /** GUI-owned interface state (selection, layout). `ui.*` parameters never reach a loader. */
  useUiParam(name: `ui.${string}`): [string | null, (value: string | null) => void];
  /** The only way to write: the host runs the form, the confirmation level and the request.
   *  Resolves { ok: false, status: 0, code: "cancelled" } when the person cancels. */
  act(ref: ActionRef, options?: { initial?: Json }): Promise<ActionResult>;
  open(link: LinkRef): Promise<void>;
  KitView: SyncComponent<ViewProps>;   // the generic renderer of all five kinds; satisfies GuiPlugin.View
  RawView: SyncComponent<ViewProps>;   // the host fallback for a kind a GUI does not know
  SignOut: ComponentType<{ className?: string }>;
}

// ── What a GUI plugin implements ────────────────────────────────────────────────────────────────

/** A component that renders synchronously, so it works in the browser, in jsdom and inside embeds. */
export type SyncNode = Exclude<ReactNode, Promise<unknown>>;
export type SyncComponent<P> = (props: P) => SyncNode;
type Sync<P> = SyncComponent<P>;
/** Shell and PublicFrame may be server components. */
type MaybeServer<P> = (props: P) => ReactNode | Promise<ReactNode>;

export interface ShellProps { manifest: Manifest; initial: InitialView; children: ReactNode }
export interface ViewProps { model: ViewModel }
export interface FormProps {
  spec: ActionSpec; actionRef: ActionRef; model: ViewModel | null;
  initial: Json | null; busy: boolean; error: { message: string; field?: string } | null;
  submit(body: Json): void;   // the kit then runs the confirmation level and the request
  cancel(): void;
}
export interface OverlayProps {
  title: string; tone: "neutral" | "danger"; role: "form" | "confirm";
  onDismiss(): void;
  children: ReactNode;        // host-rendered content; the GUI frames it (dialog, sheet, page) and never alters it
}
export type PublicPage =
  | "login" | "forgot-password" | "reset-password" | "accept-invite" | "no-access" | "setup";

/** The metadata half: src/gui/meta.ts. Read at boot without importing React. */
export interface GuiMeta {
  name: string;                    // one identifier: the map key, the Cordis plugin name and the MGM_GUI value
  title: string; version: string;
  contract: string;                // a semver range over GUI_CONTRACT, e.g. "^0.1"
  styles: string;                  // the package's stylesheet source; the host links only the active GUI's build
  dependencies: readonly string[]; // npm packages it imports, each on the reviewed list in gui-boundary.test.ts
}
/** The React half: src/gui/installed.ts. Imported only by host pages; importable from client code. */
export interface GuiPlugin extends GuiMeta {
  Shell: MaybeServer<ShellProps>;                    // MUST: persistent layout
  View: Sync<ViewProps>;                             // MUST: the five kinds (re-exporting kit.KitView counts)
  renderers?: Partial<Record<Id, { reads: readonly number[]; Component: Sync<ViewProps> }>>;  // MAY: by view id
  forms?: Partial<Record<Id, { reads: readonly number[]; Component: Sync<FormProps> }>>;      // MAY: none/confirm only
  Overlay?: Sync<OverlayProps>;                      // MAY: frames the host's forms and confirmation steps
  Loading?: Sync<Record<string, never>>;             // MAY: otherwise the host's neutral skeleton
  PublicFrame?: MaybeServer<{ page: PublicPage; children: ReactNode }>;  // MAY: server component, no client imports
}

/** Host-owned shapes. GUIs never receive these; listed so the host and capabilities share one type. */
export interface Ceremony {
  type: "ceremony";
  statement: string;
  facts: { label: string; value: Value }[];
  warnings: string[];
  acknowledgements: { id: string; label: string }[];  // rendered verbatim, unticked; the server recomputes the set
  typed?: string;                                      // the name a person types; compared again on the server
  digest?: string;                                     // recomputed by the server inside the write
}
export interface Confirmation { digest?: string; acknowledged: string[]; typed?: string }

// ════════════════════════════════════════════════════════════════════════════════════════════════
// Part B: src/lib/gui/surface.ts (server-only)
// ════════════════════════════════════════════════════════════════════════════════════════════════

/** Server Value builders (src/lib/gui/fmt.ts). The status maps move here from StatusChip. */
export interface Fmt {
  text(s: string): Value;
  empty(): Value;
  number(v: number, unit?: string): Value;
  money(minor: number, currency: string, o?: { bare?: true }): Value;
  energy(wh: number, o?: { bare?: true; at?: string; derived?: true; synthetic?: true }): Value;
  power(w: number, o?: { at?: string; derived?: true; synthetic?: true }): Value;
  percent(v: number, o?: { at?: string; synthetic?: true }): Value;
  date(isoDate: string): Value;
  datetime(iso: string, tz: string): Value;
  status(map: string, v: string): Value;
  ref(r: EntityRef): Value;
}

/** One per loader call, signal, search, prepare or tool call. */
export interface SurfaceRequest {
  supabase: SupabaseClient;   // this call's own client for the caller's session (RLS), never the service role
  user: User;                 // already checked with getUser() on this request
  roles: UserRoleRecord[];
  organizationId: string;     // from the entity, or the host-validated current organization; never null here
  params: Record<string, string>;
  search: URLSearchParams;    // `ui.*` parameters removed
  locale: string; now: Date; fmt: Fmt;
}
/** Installation-scoped views (the super admin's organization list) get no organization and may not compose. */
export type InstallationRequest = Omit<SurfaceRequest, "organizationId"> & { organizationId: null };
export type Loaded<T> =
  | { ok: true; model: T }
  | { ok: false; status: 403 | 404 | 409 | 500 | 503; message: string };

/** What a loader puts where a GUI will see an ActionRef. The host resolves label, enabled, reason, tone and from. */
export type ActionLink = Pick<ActionRef, "action" | "params" | "prefill" | "provenance"> & { label?: string };
type HostFilled = "type" | "view" | "kind" | "org" | "trail" | "attached" | "notice" | "live" | "links";
export type LoadedModel<K extends ViewKind> =
  Omit<ModelBase<K, ActionLink, unknown>, HostFilled> & { links?: LinkRef[] };

interface ViewDefBase<K extends ViewKind> {
  id: Id; kind: K;
  path: string;                            // a page route this plugin owns in bundled.ts and releases
  params: Record<string, EntityKind>;      // which segment holds which kind: { id: "microgrid", periodId: "billingPeriod" }
  entity?: string;                         // the segment naming this view's own entity
  attach?: { to: EntityKind; order: number; hint: "section" | "tab" | "tile" };
  cost: "db" | "vendor";                   // vendor: gated like a write, polled sparingly
  agent?: "read";                          // deny by default: only this makes the view an assistant read tool
  live?: { refreshSeconds: number; staleAfterSeconds: number };
  declares: { keys: readonly string[]; prefixes?: readonly string[] };  // every cell, field, tile and series key
  ext?: { schema: `${Id}@${number}`; zod: ZodType };                     // strict objects all the way down
  examples: readonly ModelOf<K>[];         // fixtures for conformance and the GUI × view matrix
}
export type ViewDefOf<K extends ViewKind> = ViewDefBase<K> & (
  | { scope?: "organization"; load(req: SurfaceRequest): Promise<Loaded<LoadedModel<K>>> }
  | { scope: "installation"; load(req: InstallationRequest): Promise<Loaded<LoadedModel<K>>> }
);
export type ViewDef = { [K in ViewKind]: ViewDefOf<K> }[ViewKind];

export type Prepare = (
  req: SurfaceRequest,
  input: { params: Record<string, string>; body: unknown },
) => Promise<Loaded<Ceremony>>;
type ActionBase = {
  id: Id; title: string;
  target: EntityKind;                     // the entity this action is offered on
  input: ZodType;                         // from src/lib/<capability>/schemas.ts, the schema the route parses with
  inputVersion: number;
  path: string;                           // an existing route.ts exporting `method`, owned and released
  bind: { path?: Record<string, EntityKind>; body?: Record<string, EntityKind> };  // filled by the host, hidden in forms
  available?(req: SurfaceRequest, ids: string[]): Promise<Record<string, true | string>>;  // true or a reason
};
type WriteMethod = "POST" | "PUT" | "PATCH";
type LightLevel =
  | { risk: "write" | "money"; confirm: "none" | "confirm"; method: WriteMethod; prepare?: Prepare }
  | { risk: "external"; confirm: "confirm"; method: WriteMethod; prepare: Prepare };  // shows who is contacted
/** The only kind the assistant may turn into a propose tool. */
export type ProposableAction = ActionBase & LightLevel & { humanOnly: false };
export type HumanAction = ActionBase & LightLevel & { humanOnly: true };
/** Typed and ceremony actions, and every DELETE or destructive action: human-only, prepared, never a tool. */
export type GuardedAction = ActionBase & {
  humanOnly: true; confirm: "typed" | "ceremony";
  risk: "write" | "money" | "external" | "destructive";
  method: WriteMethod | "DELETE";
  prepare: Prepare;
};
export type ActionDef = ProposableAction | HumanAction | GuardedAction;

type NavDef = { id: Id; title: string; icon: Icon; group: NavGroup; order: number }
  & ({ view: Id } | { href: string; needs: CoreEntityKind[]; legacy: true });
type SignalOut = SignalOf<ActionLink> extends infer S ? (S extends unknown ? Omit<S, "source"> : never) : never;

export interface Contribution {
  plugin: MgmPluginName;
  nav?: NavDef[];
  views?: ViewDef[];
  actions?: ActionDef[];
  signals?: { id: Id; budgetMs?: number; load(req: SurfaceRequest): Promise<SignalOut[]> }[];
  status?: { id: Id; load(req: SurfaceRequest): Promise<Omit<StatusItemOf<ActionLink>, "id"> | null> }[];
  search?: { prefix: "@" | "#"; entity: EntityKind; find(req: SurfaceRequest, q: string): Promise<EntityHit[]> }[];
  /** A plugin's own entity kinds and how each resolves to its organization (null: not visible → 404). */
  entities?: {
    kind: `${string}.${string}`;
    organizationOf(req: InstallationRequest, id: string): Promise<string | null>;
  }[];
}

/** The one root service the host provides. */
export interface MgmSurface {
  /** Capability plugins, inside ctx.effect. Zod-parses the contribution and throws at boot on any rule it breaks. */
  contribute(c: Contribution): () => void;
  /** One per compiled-in GUI, inside ctx.effect. Throws when the GUI's range excludes GUI_CONTRACT. */
  install(gui: GuiMeta): () => void;
}

declare module "cordis" {
  interface Context { mgmSurface: MgmSurface }
}
/** The root context type with `mgmSurface`, for the host's install and contribute plugins. */
export type SurfaceContext = Context;
