import { useEffect, useMemo } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
  ACCOUNT_SIDES,
  AFFILIATION_KINDS,
  CUSTOMER_SEGMENTS,
  DEMAND_STAGE_LABELS,
  DEMAND_STAGES,
  GPU_SOCKETS,
  INTERCONNECT_TYPES,
  PRODUCT_LINES,
  SECURITY_TIERS,
  SUPPLIER_TYPES,
  SUPPLY_STAGE_LABELS,
  SUPPLY_STAGES,
  type AccountSide,
  type ActivityType,
  type Team,
} from '@pig/core';
import { LoaderCircle, Lock } from 'lucide-react';
import { useForm, type Control, type FieldPath, type FieldValues } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
import { Badge, Input, Section } from '@/components/ui';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { ApiError, compactNumber, get, patch, post, unitPrice } from '@/lib/api';
import { can, canAny, type PermissionIdentity } from '@/lib/permissions';

export interface AccountRecord {
  id: string;
  name: string;
  domain: string | null;
  website: string | null;
  description: string | null;
  side: AccountSide;
  supplierType: (typeof SUPPLIER_TYPES)[number] | null;
  customerSegment: (typeof CUSTOMER_SEGMENTS)[number] | null;
  country: string | null;
  region: string | null;
  jurisdiction: string | null;
  ultimateParentName: string | null;
  ultimateParentCountry: string | null;
  confidence: string;
  lastActivityAt: string | null;
}

export interface ContactRecord {
  id: string;
  accountId: string | null;
  fullName: string;
  firstName: string | null;
  lastName: string | null;
  title: string | null;
  email: string | null;
  phone: string | null;
  linkedinUrl: string | null;
  twitterHandle: string | null;
  githubHandle: string | null;
  websiteUrl: string | null;
  affiliation: (typeof AFFILIATION_KINDS)[number];
  isDecisionMaker: boolean;
  confidence: string;
  confidenceNote: string | null;
  lastActivityAt: string | null;
}

export interface ContactRow {
  contact: ContactRecord;
  accountName: string | null;
  accountSide: AccountSide | null;
}

export interface DemandDealRecord {
  id: string;
  accountId: string;
  name: string;
  description: string | null;
  productLine: (typeof PRODUCT_LINES)[number];
  stage: (typeof DEMAND_STAGES)[number];
  primaryContactId: string | null;
  acvCents: number | null;
  tcvCents: number | null;
  currency: string;
  termMonths: number | null;
  probability: string | number | null;
  expectedCloseDate: string | null;
  closedReason: string | null;
  msaExecuted: boolean;
  dpaExecuted: boolean;
  parentDealId: string | null;
  updatedAt: string;
}

export interface SupplyDealRecord {
  id: string;
  accountId: string;
  siteId: string | null;
  name: string;
  stage: (typeof SUPPLY_STAGES)[number];
  primaryContactId: string | null;
  gpuType: string | null;
  gpuCount: number | null;
  interconnectType: (typeof INTERCONNECT_TYPES)[number] | null;
  targetCostPerGpuHourCents: number | null;
  termMonths: number | null;
  availableFrom: string | null;
  technicalVerdict: string | null;
  technicalNotes: string | null;
  financialVerdict: string | null;
  financialNotes: string | null;
  rejectionReason: string | null;
  updatedAt: string;
}

interface SheetProps<Record> {
  open: boolean;
  onOpenChange(open: boolean): void;
  record?: Record | null;
  identity?: PermissionIdentity;
}

const optionalEmail = z.string().refine(
  (value) => value === '' || z.string().email().safeParse(value).success,
  'Enter a complete email address or leave it blank.',
);
const optionalUrl = z.string().refine(
  (value) => value === '' || z.string().url().safeParse(value).success,
  'Include the full URL, including https://.',
);
const optionalPositiveNumber = z.string().refine(
  (value) => value === '' || (Number.isFinite(Number(value)) && Number(value) > 0),
  'Enter a number greater than zero or leave it blank.',
);
const optionalNonnegativeNumber = z.string().refine(
  (value) => value === '' || (Number.isFinite(Number(value)) && Number(value) >= 0),
  'Enter zero or a positive number.',
);
const optionalPercentage = z.string().refine(
  (value) => value === '' || (Number(value) >= 0 && Number(value) <= 100),
  'Use a percentage from 0 to 100.',
);
const requiredPositiveNumber = z.string().refine(
  (value) => Number.isFinite(Number(value)) && Number(value) > 0,
  'Enter a number greater than zero.',
);
const requiredNonnegativeNumber = z.string().refine(
  (value) => Number.isFinite(Number(value)) && Number(value) >= 0,
  'Enter zero or a positive amount.',
);
const requiredWholeNumber = z.string().refine(
  (value) => Number.isInteger(Number(value)) && Number(value) > 0,
  'Enter a whole number greater than zero.',
);
const optionalWholeNumber = z.string().refine(
  (value) => value === '' || (Number.isInteger(Number(value)) && Number(value) >= 0),
  'Enter a whole number of days or leave it blank.',
);

const accountFormSchema = z.object({
  name: z.string().trim().min(1, 'Name is required.'),
  domain: z.string(),
  website: optionalUrl,
  description: z.string(),
  side: z.enum(ACCOUNT_SIDES),
  supplierType: z.string(),
  customerSegment: z.string(),
  country: z.string(),
  region: z.string(),
  jurisdiction: z.string(),
  ultimateParentName: z.string(),
  ultimateParentCountry: z.string(),
});
type AccountForm = z.infer<typeof accountFormSchema>;

const contactFormSchema = z.object({
  accountId: z.string().uuid('Select an account.'),
  fullName: z.string().trim().min(1, 'Full name is required.'),
  firstName: z.string(),
  lastName: z.string(),
  title: z.string(),
  email: optionalEmail,
  phone: z.string(),
  linkedinUrl: optionalUrl,
  twitterHandle: z.string(),
  githubHandle: z.string(),
  websiteUrl: optionalUrl,
  affiliation: z.enum(AFFILIATION_KINDS),
  isDecisionMaker: z.boolean(),
  confidenceNote: z.string(),
});
type ContactForm = z.infer<typeof contactFormSchema>;

const demandFormSchema = z.object({
  accountId: z.string().uuid('Select a customer account.'),
  name: z.string().trim().min(1, 'Deal name is required.'),
  description: z.string(),
  productLine: z.enum(PRODUCT_LINES),
  stage: z.enum(DEMAND_STAGES),
  primaryContactId: z.string(),
  acv: optionalNonnegativeNumber,
  tcv: optionalNonnegativeNumber,
  currency: z.string().trim().length(3, 'Use a three-letter currency code.'),
  termMonths: optionalPositiveNumber,
  probability: optionalPercentage,
  expectedCloseDate: z.string(),
  closedReason: z.string(),
  msaExecuted: z.boolean(),
  dpaExecuted: z.boolean(),
  parentDealId: z.string(),
});
type DemandForm = z.infer<typeof demandFormSchema>;

const supplyFormSchema = z.object({
  accountId: z.string().uuid('Select a supplier account.'),
  name: z.string().trim().min(1, 'Deal name is required.'),
  stage: z.enum(SUPPLY_STAGES),
  primaryContactId: z.string(),
  gpuType: z.string(),
  gpuCount: optionalPositiveNumber,
  interconnectType: z.string(),
  targetCost: optionalNonnegativeNumber,
  termMonths: optionalPositiveNumber,
  availableFrom: z.string(),
  technicalVerdict: z.string(),
  technicalNotes: z.string(),
  financialVerdict: z.string(),
  financialNotes: z.string(),
  rejectionReason: z.string(),
});
type SupplyForm = z.infer<typeof supplyFormSchema>;

/**
 * Hours between two `datetime-local` values, or null while either is unset or
 * inverted. Derived from the same instants that get POSTed, so the envelope
 * shown to the buyer cannot disagree with the one the server checks — including
 * across a daylight-saving boundary, where a 90-day block is not 2,160 hours.
 */
function windowHours(startsAt: string, endsAt: string): number | null {
  const start = new Date(startsAt).getTime();
  const end = new Date(endsAt).getTime();
  if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;
  return (end - start) / 3_600_000;
}

/** The most GPU-hours a flat (unshaped) block of this size can hold. */
function flatEnvelopeGpuHours(startsAt: string, endsAt: string, gpuCount: string): number | null {
  const hours = windowHours(startsAt, endsAt);
  const count = Number(gpuCount);
  if (hours === null || !Number.isFinite(count) || count <= 0) return null;
  return hours * count;
}

const commitmentFormSchema = z
  .object({
    accountId: z.string().uuid('Select a supplier account.'),
    supplyDealId: z.string(),
    name: z.string().trim().min(1, 'Name the commitment.').max(200, 'Keep the name under 200 characters.'),
    gpuType: z.string().trim().min(1, 'GPU type is required.'),
    socket: z.string(),
    gpuCount: requiredWholeNumber,
    interconnectType: z.enum(INTERCONNECT_TYPES),
    securityTier: z.enum(SECURITY_TIERS),
    startsAt: z.string().min(1, 'Start is required.'),
    endsAt: z.string().min(1, 'End is required.'),
    totalGpuHours: requiredPositiveNumber,
    costPerGpuHour: requiredNonnegativeNumber,
    currency: z.string().trim().length(3, 'Use a three-letter currency code.'),
    isContiguous: z.boolean(),
    oversubscriptionPct: z.string().refine(
      (value) => value === '' || (Number(value) >= 0 && Number(value) <= 1000),
      'Use a percentage from 0 to 1000.',
    ),
    minimumSpend: optionalNonnegativeNumber,
    takeOrPayFloorPct: optionalPercentage,
    prepaidPct: optionalPercentage,
    prepaidAmount: optionalNonnegativeNumber,
    noticeDays: optionalWholeNumber,
    isAutoRenew: z.boolean(),
    notes: z.string().max(10_000, 'Keep notes under 10,000 characters.'),
  })
  .superRefine((values, context) => {
    if (values.startsAt && values.endsAt && windowHours(values.startsAt, values.endsAt) === null) {
      context.addIssue({ code: 'custom', path: ['endsAt'], message: 'End must be after start.' });
    }
    const envelope = flatEnvelopeGpuHours(values.startsAt, values.endsAt, values.gpuCount);
    const total = Number(values.totalGpuHours);
    // The server refuses this outright, and it is the easy mistake to make:
    // contracted GPU-hours are a total for the term, not a per-day figure.
    if (envelope !== null && total - envelope > 1e-7) {
      context.addIssue({
        code: 'custom',
        path: ['totalGpuHours'],
        message: `${values.gpuCount} GPUs over this window hold at most ${Math.floor(envelope).toLocaleString()} GPU-hours.`,
      });
    }
    if (Math.abs(total * 100 - Math.round(total * 100)) > 1e-7) {
      context.addIssue({
        code: 'custom',
        path: ['totalGpuHours'],
        message: 'GPU-hours may have at most two decimal places.',
      });
    }
  });
type CommitmentForm = z.infer<typeof commitmentFormSchema>;

/**
 * The types a person logs by hand.
 *
 * The rest of `ACTIVITY_TYPES` are written by the system — a stage change, a
 * contract event, an agent's action — and offering them here would let a typed
 * note claim machine provenance in a timeline that is read as an audit trail.
 */
const LOGGABLE_ACTIVITY_TYPES = [
  'call',
  'meeting',
  'email',
  'note',
] as const satisfies readonly ActivityType[];

const ACTIVITY_TYPE_HINTS: Record<(typeof LOGGABLE_ACTIVITY_TYPES)[number], string> = {
  call: 'What was said, and what was agreed.',
  meeting: 'Who attended, and what changed as a result.',
  email: 'Paste the substance, not the thread.',
  note: 'Something learned that belongs on the record.',
};

const activityFormSchema = z.object({
  type: z.enum(LOGGABLE_ACTIVITY_TYPES),
  accountId: z.string().uuid('Select the account this belongs to.'),
  relatedDeal: z.string(),
  contactId: z.string(),
  subject: z.string().trim().min(1, 'Give it a subject.').max(200, 'Keep the subject under 200 characters.'),
  body: z.string().max(8_000, 'Keep the detail under 8,000 characters.'),
  occurredAt: z.string().min(1, 'Record when it happened.'),
});
type ActivityForm = z.infer<typeof activityFormSchema>;

// `ai` is cased because the enum is `applied_ai_startup`, and an option
// reading "Applied ai startup" in a form about GPU capacity looks like a typo.
const label = (value: string) => value.replace(/_/g, ' ').replace(/\bai\b/g, 'AI').replace(/^./, (letter) => letter.toUpperCase());
const blankToNull = (value: string) => value.trim() || null;
const optionalNumber = (value: string) => value === '' ? null : Number(value);
const cents = (value: string) => value === '' ? null : Math.round(Number(value) * 100);
const dollars = (value: number | null | undefined) => value == null ? '' : String(value / 100);
const dateInput = (value: string | null | undefined) => value ? value.slice(0, 10) : '';

function canWriteSide(identity: PermissionIdentity | undefined, side: AccountSide): boolean {
  return (
    (side !== 'supply' && can(identity, 'deal:write', 'demand')) ||
    (side !== 'demand' && can(identity, 'deal:write', 'supply'))
  );
}

/**
 * Mirrors `requireSidePermission` on the server: an activity is a supply-side
 * or demand-side event, and a `both` account admits either. Asking the same
 * question here means the control is absent rather than answering 403.
 *
 * Exported so a page gating a "Log activity" affordance on a specific account
 * asks exactly the question the server will ask about that account.
 */
export function canLogAgainstSide(identity: PermissionIdentity | undefined, side: AccountSide): boolean {
  const teams: Team[] = side === 'both' ? ['supply', 'demand'] : [side];
  return teams.some((team) => can(identity, 'activity:write', team));
}

/**
 * `datetime-local` carries no time zone: it wants wall-clock time. Subtracting
 * the offset before slicing is what stops the field opening hours adrift of
 * the clock on the wall, which is the one thing a logged call must match.
 */
function localDateTimeValue(date: Date): string {
  return new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString().slice(0, 16);
}

/**
 * A single "related deal" control writes one of two columns. Encoding the side
 * into the option value keeps that a choice rather than two selects the user
 * could fill in contradictory ways.
 */
function dealReference(value: string): { demandDealId?: string; supplyDealId?: string } {
  const [side, id] = value.split(':');
  if (!id) return {};
  return side === 'supply' ? { supplyDealId: id } : { demandDealId: id };
}

function accountDefaults(record?: AccountRecord | null): AccountForm {
  return {
    name: record?.name ?? '',
    domain: record?.domain ?? '',
    website: record?.website ?? '',
    description: record?.description ?? '',
    side: record?.side ?? 'demand',
    supplierType: record?.supplierType ?? '',
    customerSegment: record?.customerSegment ?? '',
    country: record?.country ?? '',
    region: record?.region ?? '',
    jurisdiction: record?.jurisdiction ?? '',
    ultimateParentName: record?.ultimateParentName ?? '',
    ultimateParentCountry: record?.ultimateParentCountry ?? '',
  };
}

export function AccountSheet({ open, onOpenChange, record, identity }: SheetProps<AccountRecord>) {
  const queryClient = useQueryClient();
  const form = useForm<AccountForm>({
    resolver: zodResolver(accountFormSchema),
    defaultValues: accountDefaults(record),
  });

  useEffect(() => {
    if (open) form.reset(accountDefaults(record));
  }, [form, open, record]);

  const save = useMutation({
    mutationFn: (values: AccountForm) => {
      const body = {
        ...values,
        domain: blankToNull(values.domain),
        website: blankToNull(values.website),
        description: blankToNull(values.description),
        supplierType: values.side === 'demand' ? null : blankToNull(values.supplierType),
        customerSegment: values.side === 'supply' ? null : blankToNull(values.customerSegment),
        country: blankToNull(values.country),
        region: blankToNull(values.region),
        jurisdiction: blankToNull(values.jurisdiction),
        ultimateParentName: blankToNull(values.ultimateParentName),
        ultimateParentCountry: blankToNull(values.ultimateParentCountry),
      };
      return record ? patch<AccountRecord>(`/api/accounts/${record.id}`, body) : post<AccountRecord>('/api/accounts', body);
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['accounts'] });
      toast.success(record ? 'Account updated' : 'Account created');
      onOpenChange(false);
    },
    onError: (error) => toast.error(errorMessage(error)),
  });

  const side = form.watch('side');
  return (
    <RecordSheet open={open} onOpenChange={onOpenChange} category="Relationship" title={record ? 'Edit account' : 'New account'} description="Keep the commercial side explicit. It controls which team can work the record and where its deals belong.">
      <Form {...form}>
        <form className="flex min-h-0 flex-1 flex-col" onSubmit={form.handleSubmit((values) => save.mutate(values))}>
          <SheetBody>
            <FieldGrid>
              <TextField control={form.control} name="name" label="Account name" placeholder="Acme AI" className="sm:col-span-2" />
              <SelectField control={form.control} name="side" label="Commercial side" options={ACCOUNT_SIDES.map((value) => ({ value, label: label(value), disabled: !canWriteSide(identity, value) }))} />
              <TextField control={form.control} name="domain" label="Domain" placeholder="acme.ai" />
              {side !== 'demand' ? <SelectField control={form.control} name="supplierType" label="Supplier type" optional options={SUPPLIER_TYPES.map((value) => ({ value, label: label(value) }))} /> : null}
              {side !== 'supply' ? <SelectField control={form.control} name="customerSegment" label="Customer segment" optional options={CUSTOMER_SEGMENTS.map((value) => ({ value, label: label(value) }))} /> : null}
              <TextField control={form.control} name="website" label="Website" placeholder="https://acme.ai" className="sm:col-span-2" />
              <TextAreaField control={form.control} name="description" label="Relationship context" placeholder="What they build, what they buy or sell, and why the relationship matters." className="sm:col-span-2" />
            </FieldGrid>
            <FormSection title="Commercial geography" description="Headquarters and legal jurisdiction are separate because export controls and data residency attach differently.">
              <FieldGrid>
                <TextField control={form.control} name="country" label="Headquarters country" />
                <TextField control={form.control} name="region" label="Region" />
                <TextField control={form.control} name="jurisdiction" label="Legal jurisdiction" className="sm:col-span-2" />
              </FieldGrid>
            </FormSection>
            <FormSection title="Ultimate ownership" description="Only enter ownership you can substantiate; the compliance engine must not infer it from headquarters.">
              <FieldGrid>
                <TextField control={form.control} name="ultimateParentName" label="Ultimate parent" />
                <TextField control={form.control} name="ultimateParentCountry" label="Parent country" />
              </FieldGrid>
            </FormSection>
          </SheetBody>
          <SheetActions pending={save.isPending} onCancel={() => onOpenChange(false)} label={record ? 'Save account' : 'Create account'} />
        </form>
      </Form>
    </RecordSheet>
  );
}

function contactDefaults(record?: ContactRecord | null, accountId?: string): ContactForm {
  return {
    accountId: record?.accountId ?? accountId ?? '',
    fullName: record?.fullName ?? '',
    firstName: record?.firstName ?? '',
    lastName: record?.lastName ?? '',
    title: record?.title ?? '',
    email: record?.email ?? '',
    phone: record?.phone ?? '',
    linkedinUrl: record?.linkedinUrl ?? '',
    twitterHandle: record?.twitterHandle ?? '',
    githubHandle: record?.githubHandle ?? '',
    websiteUrl: record?.websiteUrl ?? '',
    affiliation: record?.affiliation ?? 'unknown',
    isDecisionMaker: record?.isDecisionMaker ?? false,
    confidenceNote: record?.confidenceNote ?? '',
  };
}

export function ContactSheet({ open, onOpenChange, record, identity, defaultAccountId }: SheetProps<ContactRecord> & { defaultAccountId?: string }) {
  const queryClient = useQueryClient();
  const { data: accountsData } = useQuery({
    queryKey: ['accounts', 'all-record-options'],
    queryFn: () => get<AccountRecord[]>('/api/accounts'),
    enabled: open,
  });
  const writableAccounts = useMemo(() => (accountsData ?? []).filter((account) => canWriteSide(identity, account.side)), [accountsData, identity]);
  const form = useForm<ContactForm>({ resolver: zodResolver(contactFormSchema), defaultValues: contactDefaults(record, defaultAccountId) });
  useEffect(() => {
    if (open) form.reset(contactDefaults(record, defaultAccountId));
  }, [defaultAccountId, form, open, record]);

  const save = useMutation({
    mutationFn: (values: ContactForm) => {
      const body = {
        ...values,
        firstName: blankToNull(values.firstName), lastName: blankToNull(values.lastName), title: blankToNull(values.title),
        email: blankToNull(values.email), phone: blankToNull(values.phone), linkedinUrl: blankToNull(values.linkedinUrl),
        twitterHandle: blankToNull(values.twitterHandle), githubHandle: blankToNull(values.githubHandle), websiteUrl: blankToNull(values.websiteUrl),
        confidenceNote: blankToNull(values.confidenceNote),
      };
      return record ? patch<ContactRecord>(`/api/contacts/${record.id}`, body) : post<ContactRecord>('/api/contacts', body);
    },
    onSuccess: async () => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ['contacts'] }),
        queryClient.invalidateQueries({ queryKey: ['accounts'] }),
      ]);
      toast.success(record ? 'Contact updated' : 'Contact created');
      onOpenChange(false);
    },
    onError: (error) => toast.error(errorMessage(error)),
  });

  return (
    <RecordSheet open={open} onOpenChange={onOpenChange} category="Person" title={record ? 'Edit contact' : 'New contact'} description="Record only what is known. PIG never guesses a real person’s address or employment relationship.">
      <Form {...form}>
        <form className="flex min-h-0 flex-1 flex-col" onSubmit={form.handleSubmit((values) => save.mutate(values))}>
          <SheetBody>
            <FieldGrid>
              <SelectField control={form.control} name="accountId" label="Account" className="sm:col-span-2" options={writableAccounts.map((account) => ({ value: account.id, label: `${account.name} · ${label(account.side)}` }))} />
              <TextField control={form.control} name="fullName" label="Full name" className="sm:col-span-2" />
              <TextField control={form.control} name="firstName" label="First name" />
              <TextField control={form.control} name="lastName" label="Last name" />
              <TextField control={form.control} name="title" label="Title" />
              <SelectField control={form.control} name="affiliation" label="Affiliation" options={AFFILIATION_KINDS.map((value) => ({ value, label: label(value) }))} />
              <TextField control={form.control} name="email" label="Email" type="email" description="Leave blank unless the address is sourced or provided. Never infer it from a name and domain." className="sm:col-span-2" />
              <TextField control={form.control} name="phone" label="Phone" />
              <SwitchField control={form.control} name="isDecisionMaker" label="Decision maker" description="They can materially approve or block this relationship." />
            </FieldGrid>
            <FormSection title="Public profiles">
              <FieldGrid>
                <TextField control={form.control} name="linkedinUrl" label="LinkedIn URL" className="sm:col-span-2" />
                <TextField control={form.control} name="twitterHandle" label="X / Twitter handle" />
                <TextField control={form.control} name="githubHandle" label="GitHub handle" />
                <TextField control={form.control} name="websiteUrl" label="Website URL" className="sm:col-span-2" />
              </FieldGrid>
            </FormSection>
            <TextAreaField control={form.control} name="confidenceNote" label="Provenance note" description="Use this when the relationship or details need qualification." />
          </SheetBody>
          <SheetActions pending={save.isPending} onCancel={() => onOpenChange(false)} label={record ? 'Save contact' : 'Create contact'} />
        </form>
      </Form>
    </RecordSheet>
  );
}

function demandDefaults(record?: DemandDealRecord | null): DemandForm {
  return {
    accountId: record?.accountId ?? '', name: record?.name ?? '', description: record?.description ?? '',
    productLine: record?.productLine ?? 'compute_reserved', stage: record?.stage ?? 'qualification',
    primaryContactId: record?.primaryContactId ?? '', acv: dollars(record?.acvCents), tcv: dollars(record?.tcvCents),
    currency: record?.currency ?? 'USD', termMonths: record?.termMonths == null ? '' : String(record.termMonths),
    probability: record?.probability == null ? '' : String(Number(record.probability) * 100), expectedCloseDate: dateInput(record?.expectedCloseDate),
    closedReason: record?.closedReason ?? '', msaExecuted: record?.msaExecuted ?? false, dpaExecuted: record?.dpaExecuted ?? false,
    parentDealId: record?.parentDealId ?? '',
  };
}

export function DemandDealSheet({ open, onOpenChange, record }: SheetProps<DemandDealRecord>) {
  const queryClient = useQueryClient();
  const { data: accountData } = useQuery({ queryKey: ['accounts', 'demand-record-options'], queryFn: () => get<AccountRecord[]>('/api/accounts?side=demand'), enabled: open });
  const { data: contactRows } = useQuery({ queryKey: ['contacts', 'demand-record-options'], queryFn: () => get<ContactRow[]>('/api/contacts'), enabled: open });
  const { data: board } = useQuery({ queryKey: ['/api/deals/demand'], queryFn: () => get<{ deals: { deal: DemandDealRecord }[] }>('/api/deals/demand'), enabled: open });
  const form = useForm<DemandForm>({ resolver: zodResolver(demandFormSchema), defaultValues: demandDefaults(record) });
  useEffect(() => { if (open) form.reset(demandDefaults(record)); }, [form, open, record]);
  const accountId = form.watch('accountId');
  const contactOptions = (contactRows ?? []).filter((row) => row.contact.accountId === accountId);
  const parentOptions = (board?.deals ?? []).filter((row) => row.deal.accountId === accountId && row.deal.id !== record?.id);

  const save = useMutation({
    mutationFn: (values: DemandForm) => {
      const body = {
        accountId: values.accountId, name: values.name, description: blankToNull(values.description), productLine: values.productLine, stage: values.stage,
        primaryContactId: blankToNull(values.primaryContactId), acvCents: cents(values.acv), tcvCents: cents(values.tcv), currency: values.currency.toUpperCase(),
        termMonths: optionalNumber(values.termMonths), probability: values.probability === '' ? null : Number(values.probability) / 100,
        expectedCloseDate: blankToNull(values.expectedCloseDate), closedReason: blankToNull(values.closedReason), msaExecuted: values.msaExecuted,
        dpaExecuted: values.dpaExecuted, parentDealId: blankToNull(values.parentDealId),
      };
      return record ? patch<DemandDealRecord>(`/api/deals/demand/${record.id}`, body) : post<DemandDealRecord>('/api/deals/demand', body);
    },
    onSuccess: async () => {
      await Promise.all([queryClient.invalidateQueries({ queryKey: ['/api/deals/demand'] }), queryClient.invalidateQueries({ queryKey: ['accounts'] })]);
      toast.success(record ? 'Demand deal updated' : 'Demand deal created'); onOpenChange(false);
    },
    onError: (error) => toast.error(errorMessage(error)),
  });

  return (
    <RecordSheet open={open} onOpenChange={onOpenChange} category="Demand · sell-side" title={record ? 'Edit demand deal' : 'New demand deal'} description="Capture the commercial case and paper state. Capacity requirements remain separate so the matcher can reason about the technical shape.">
      <Form {...form}>
        <form className="flex min-h-0 flex-1 flex-col" onSubmit={form.handleSubmit((values) => save.mutate(values))}>
          <SheetBody>
            <FieldGrid>
              <SelectField control={form.control} name="accountId" label="Customer account" className="sm:col-span-2" options={(accountData ?? []).map((account) => ({ value: account.id, label: account.name }))} />
              <TextField control={form.control} name="name" label="Deal name" className="sm:col-span-2" />
              <SelectField control={form.control} name="productLine" label="Product line" options={PRODUCT_LINES.map((value) => ({ value, label: label(value) }))} />
              <SelectField control={form.control} name="stage" label="Stage" options={DEMAND_STAGES.map((value) => ({ value, label: DEMAND_STAGE_LABELS[value] }))} />
              <SelectField control={form.control} name="primaryContactId" label="Primary contact" optional className="sm:col-span-2" options={contactOptions.map((row) => ({ value: row.contact.id, label: `${row.contact.fullName}${row.contact.title ? ` · ${row.contact.title}` : ''}` }))} />
              <TextAreaField control={form.control} name="description" label="Deal context" className="sm:col-span-2" />
            </FieldGrid>
            <FormSection title="Commercials" description="Money is converted to integer cents at the API boundary; probability stays independent of stage.">
              <FieldGrid>
                <TextField control={form.control} name="acv" label="ACV" inputMode="decimal" prefix="$" />
                <TextField control={form.control} name="tcv" label="TCV" inputMode="decimal" prefix="$" />
                <TextField control={form.control} name="currency" label="Currency" maxLength={3} />
                <TextField control={form.control} name="termMonths" label="Term (months)" inputMode="numeric" />
                <TextField control={form.control} name="probability" label="Probability (%)" inputMode="decimal" />
                <TextField control={form.control} name="expectedCloseDate" label="Expected close" type="date" />
              </FieldGrid>
            </FormSection>
            <FormSection title="Paper and continuity" description="Legal clears early in this market. These flags remain visible after the deal advances.">
              <FieldGrid>
                <SwitchField control={form.control} name="msaExecuted" label="MSA executed" />
                <SwitchField control={form.control} name="dpaExecuted" label="DPA executed" />
                <SelectField control={form.control} name="parentDealId" label="Parent deal" optional className="sm:col-span-2" options={parentOptions.map((row) => ({ value: row.deal.id, label: row.deal.name }))} />
                <TextAreaField control={form.control} name="closedReason" label="Closed reason" description="Record why a deal was won or lost; leave blank while it is open." className="sm:col-span-2" />
              </FieldGrid>
            </FormSection>
          </SheetBody>
          <SheetActions pending={save.isPending} onCancel={() => onOpenChange(false)} label={record ? 'Save demand deal' : 'Create demand deal'} />
        </form>
      </Form>
    </RecordSheet>
  );
}

function supplyDefaults(record?: SupplyDealRecord | null): SupplyForm {
  return {
    accountId: record?.accountId ?? '', name: record?.name ?? '', stage: record?.stage ?? 'sourced', primaryContactId: record?.primaryContactId ?? '',
    gpuType: record?.gpuType ?? '', gpuCount: record?.gpuCount == null ? '' : String(record.gpuCount), interconnectType: record?.interconnectType ?? '',
    targetCost: dollars(record?.targetCostPerGpuHourCents), termMonths: record?.termMonths == null ? '' : String(record.termMonths), availableFrom: dateInput(record?.availableFrom),
    technicalVerdict: record?.technicalVerdict ?? '', technicalNotes: record?.technicalNotes ?? '', financialVerdict: record?.financialVerdict ?? '',
    financialNotes: record?.financialNotes ?? '', rejectionReason: record?.rejectionReason ?? '',
  };
}

export function SupplyDealSheet({ open, onOpenChange, record }: SheetProps<SupplyDealRecord>) {
  const queryClient = useQueryClient();
  const { data: accountData } = useQuery({ queryKey: ['accounts', 'supply-record-options'], queryFn: () => get<AccountRecord[]>('/api/accounts?side=supply'), enabled: open });
  const { data: contactRows } = useQuery({ queryKey: ['contacts', 'supply-record-options'], queryFn: () => get<ContactRow[]>('/api/contacts'), enabled: open });
  const form = useForm<SupplyForm>({ resolver: zodResolver(supplyFormSchema), defaultValues: supplyDefaults(record) });
  useEffect(() => { if (open) form.reset(supplyDefaults(record)); }, [form, open, record]);
  const accountId = form.watch('accountId');
  const contactOptions = (contactRows ?? []).filter((row) => row.contact.accountId === accountId);

  const save = useMutation({
    mutationFn: (values: SupplyForm) => {
      const body = {
        accountId: values.accountId, name: values.name, stage: values.stage, primaryContactId: blankToNull(values.primaryContactId), siteId: record?.siteId ?? null,
        gpuType: blankToNull(values.gpuType), gpuCount: optionalNumber(values.gpuCount), interconnectType: blankToNull(values.interconnectType),
        targetCostPerGpuHourCents: cents(values.targetCost), termMonths: optionalNumber(values.termMonths), availableFrom: blankToNull(values.availableFrom),
        technicalVerdict: blankToNull(values.technicalVerdict), technicalNotes: blankToNull(values.technicalNotes), financialVerdict: blankToNull(values.financialVerdict),
        financialNotes: blankToNull(values.financialNotes), rejectionReason: blankToNull(values.rejectionReason),
      };
      return record ? patch<SupplyDealRecord>(`/api/deals/supply/${record.id}`, body) : post<SupplyDealRecord>('/api/deals/supply', body);
    },
    onSuccess: async () => {
      await Promise.all([queryClient.invalidateQueries({ queryKey: ['/api/deals/supply'] }), queryClient.invalidateQueries({ queryKey: ['accounts'] })]);
      toast.success(record ? 'Supply deal updated' : 'Supply deal created'); onOpenChange(false);
    },
    onError: (error) => toast.error(errorMessage(error)),
  });

  return (
    <RecordSheet open={open} onOpenChange={onOpenChange} category="Supply · buy-side" title={record ? 'Edit supply deal' : 'New supply deal'} description="Qualify the capacity and economics independently. A supplier relationship is not interchangeable with a customer opportunity.">
      <Form {...form}>
        <form className="flex min-h-0 flex-1 flex-col" onSubmit={form.handleSubmit((values) => save.mutate(values))}>
          <SheetBody>
            <FieldGrid>
              <SelectField control={form.control} name="accountId" label="Supplier account" className="sm:col-span-2" options={(accountData ?? []).map((account) => ({ value: account.id, label: account.name }))} />
              <TextField control={form.control} name="name" label="Deal name" className="sm:col-span-2" />
              <SelectField control={form.control} name="stage" label="Stage" options={SUPPLY_STAGES.map((value) => ({ value, label: SUPPLY_STAGE_LABELS[value] }))} />
              <SelectField control={form.control} name="primaryContactId" label="Primary contact" optional options={contactOptions.map((row) => ({ value: row.contact.id, label: row.contact.fullName }))} />
            </FieldGrid>
            <FormSection title="Capacity on offer" description="These terms describe the opportunity, not booked inventory. A commitment is created only after paper is executed.">
              <FieldGrid>
                <TextField control={form.control} name="gpuType" label="GPU type" placeholder="H100_80GB" />
                <TextField control={form.control} name="gpuCount" label="GPU count" inputMode="numeric" />
                <SelectField control={form.control} name="interconnectType" label="Interconnect" optional options={INTERCONNECT_TYPES.map((value) => ({ value, label: value }))} />
                <TextField control={form.control} name="targetCost" label="Target $ / GPU-hr" inputMode="decimal" prefix="$" />
                <TextField control={form.control} name="termMonths" label="Term (months)" inputMode="numeric" />
                <TextField control={form.control} name="availableFrom" label="Available from" type="date" />
              </FieldGrid>
            </FormSection>
            <FormSection title="Two-key diligence" description="Technical fitness and financial clearance are independent decisions. Record each verdict in its own voice.">
              <FieldGrid>
                <TextField control={form.control} name="technicalVerdict" label="Technical verdict" placeholder="Passed, conditional, blocked…" />
                <TextField control={form.control} name="financialVerdict" label="Financial verdict" placeholder="Passed, conditional, blocked…" />
                <TextAreaField control={form.control} name="technicalNotes" label="Technical notes" />
                <TextAreaField control={form.control} name="financialNotes" label="Financial notes" />
              </FieldGrid>
            </FormSection>
            <TextAreaField control={form.control} name="rejectionReason" label="Rejection reason" description="Rejections teach the sourcing team. Leave blank unless the deal is rejected." />
          </SheetBody>
          <SheetActions pending={save.isPending} onCancel={() => onOpenChange(false)} label={record ? 'Save supply deal' : 'Create supply deal'} />
        </form>
      </Form>
    </RecordSheet>
  );
}

export interface CapacityCommitmentRecord {
  id: string;
  name: string;
  totalGpuHours: string | number;
  costPerGpuHourCents: number;
}

interface CreateSheetProps {
  open: boolean;
  onOpenChange(open: boolean): void;
  identity?: PermissionIdentity;
  defaultAccountId?: string;
}

function commitmentDefaults(accountId?: string): CommitmentForm {
  return {
    accountId: accountId ?? '', supplyDealId: '', name: '', gpuType: '', socket: '', gpuCount: '',
    interconnectType: 'Unknown', securityTier: 'secure_cloud', startsAt: '', endsAt: '', totalGpuHours: '',
    costPerGpuHour: '', currency: 'USD', isContiguous: true, oversubscriptionPct: '', minimumSpend: '',
    takeOrPayFloorPct: '', prepaidPct: '', prepaidAmount: '', noticeDays: '', isAutoRenew: false, notes: '',
  };
}

/**
 * Recording capacity we have committed to buy.
 *
 * This is the root record of the supply side: availability, the matcher, every
 * margin figure and the idle-capacity alerts are all derived from these blocks,
 * so until one exists the product has nothing to show. `commitment:write` is
 * supply-team and lead-and-above by policy, which is why the team is named here
 * rather than inferred — see TEAM_CAPABILITY_RULES.
 */
export function CommitmentSheet({ open, onOpenChange, identity, defaultAccountId }: CreateSheetProps) {
  const queryClient = useQueryClient();
  const writable = can(identity, 'commitment:write', 'supply');
  const { data: accountData } = useQuery({ queryKey: ['accounts', 'commitment-record-options'], queryFn: () => get<AccountRecord[]>('/api/accounts?side=supply'), enabled: open });
  const { data: supplyBoard } = useQuery({ queryKey: ['/api/deals/supply'], queryFn: () => get<{ deals: { deal: SupplyDealRecord }[] }>('/api/deals/supply'), enabled: open });
  const form = useForm<CommitmentForm>({ resolver: zodResolver(commitmentFormSchema), defaultValues: commitmentDefaults(defaultAccountId) });
  useEffect(() => { if (open) form.reset(commitmentDefaults(defaultAccountId)); }, [defaultAccountId, form, open]);
  const accountId = form.watch('accountId');
  const dealOptions = (supplyBoard?.deals ?? []).filter((row) => row.deal.accountId === accountId);
  const envelope = flatEnvelopeGpuHours(form.watch('startsAt'), form.watch('endsAt'), form.watch('gpuCount'));

  const save = useMutation({
    mutationFn: (values: CommitmentForm) =>
      post<CapacityCommitmentRecord>('/api/commitments', {
        accountId: values.accountId, supplyDealId: blankToNull(values.supplyDealId), name: values.name.trim(),
        gpuType: values.gpuType.trim(), socket: blankToNull(values.socket), gpuCount: Number(values.gpuCount),
        interconnectType: values.interconnectType, securityTier: values.securityTier,
        startsAt: new Date(values.startsAt).toISOString(), endsAt: new Date(values.endsAt).toISOString(),
        totalGpuHours: Number(values.totalGpuHours), costPerGpuHourCents: Math.round(Number(values.costPerGpuHour) * 100),
        currency: values.currency.toUpperCase(), isContiguous: values.isContiguous,
        oversubscriptionPct: values.oversubscriptionPct === '' ? 0 : Number(values.oversubscriptionPct),
        minimumSpendCents: cents(values.minimumSpend), takeOrPayFloorPct: optionalNumber(values.takeOrPayFloorPct),
        prepaidPct: optionalNumber(values.prepaidPct), prepaidAmountCents: cents(values.prepaidAmount),
        noticeDays: optionalNumber(values.noticeDays), isAutoRenew: values.isAutoRenew, notes: blankToNull(values.notes),
      }),
    onSuccess: async (commitment) => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ['availability'] }),
        queryClient.invalidateQueries({ queryKey: ['commitments'] }),
        queryClient.invalidateQueries({ queryKey: ['margin'] }),
        queryClient.invalidateQueries({ queryKey: ['dashboard'] }),
        queryClient.invalidateQueries({ queryKey: ['accounts'] }),
      ]);
      // The block is only worth recording because it can now be sold, so say
      // that rather than "saved" — the seller's next move is the matcher.
      toast.success('Capacity commitment recorded', {
        description: `${compactNumber(Number(commitment.totalGpuHours))} GPU-hrs at ${unitPrice(commitment.costPerGpuHourCents)}/GPU-hr are now sellable and carried in margin.`,
      });
      onOpenChange(false);
    },
    onError: (error) => toast.error(errorMessage(error)),
  });

  return (
    <RecordSheet open={open} onOpenChange={onOpenChange} category="Supply · committed capacity" title="Record capacity commitment" description="Capacity we have contracted to buy. Availability, the matcher and every margin figure are derived from these blocks.">
      <Form {...form}>
        <form className="flex min-h-0 flex-1 flex-col" onSubmit={form.handleSubmit((values) => save.mutate(values))}>
          <SheetBody>
            {writable ? null : <PermissionNotice>Recording committed capacity needs supply-team lead access. A platform administrator can grant it.</PermissionNotice>}
            <FieldGrid>
              <SelectField control={form.control} name="accountId" label="Supplier account" className="sm:col-span-2" options={(accountData ?? []).map((account) => ({ value: account.id, label: account.name }))} />
              <SelectField control={form.control} name="supplyDealId" label="Originating supply deal" optional className="sm:col-span-2" options={dealOptions.map((row) => ({ value: row.deal.id, label: row.deal.name }))} />
              <TextField control={form.control} name="name" label="Commitment name" placeholder="CoreWeave H100 · Q4 block" className="sm:col-span-2" />
              {/* Hardware identifiers are case-sensitive upstream; correcting
                  them for the user would produce silent mismatches. */}
              <TextField control={form.control} name="gpuType" label="GPU type" placeholder="H100_80GB" autoCapitalize="off" autoCorrect="off" spellCheck={false} />
              <TextField control={form.control} name="gpuCount" label="GPU count" inputMode="numeric" />
              <SelectField control={form.control} name="socket" label="Socket" optional options={GPU_SOCKETS.map((value) => ({ value, label: value }))} />
              <SelectField control={form.control} name="interconnectType" label="Interconnect" options={INTERCONNECT_TYPES.map((value) => ({ value, label: value }))} />
              <SelectField control={form.control} name="securityTier" label="Security tier" options={SECURITY_TIERS.map((value) => ({ value, label: label(value) }))} />
              <SwitchField control={form.control} name="isContiguous" label="Contiguous block" description="Not one GPU count split across halls." />
            </FieldGrid>
            <FormSection title="Term and envelope" description="Contracted GPU-hours are stored as entered, not derived: ramp periods, maintenance windows and holdbacks are real and no formula predicts them.">
              <FieldGrid>
                <TextField control={form.control} name="startsAt" label="Starts" type="datetime-local" />
                <TextField control={form.control} name="endsAt" label="Ends" type="datetime-local" />
                <TextField control={form.control} name="totalGpuHours" label="Contracted GPU-hours" inputMode="decimal" description={envelope === null ? 'Set the window and GPU count to see the flat envelope.' : `A flat block this size holds ${compactNumber(envelope)} GPU-hrs at most.`} />
                <TextField control={form.control} name="costPerGpuHour" label="Cost $ / GPU-hr" inputMode="decimal" prefix="$" />
                <TextField control={form.control} name="currency" label="Currency" maxLength={3} />
                <TextField control={form.control} name="oversubscriptionPct" label="Oversubscription allowance (%)" inputMode="decimal" description="Leave blank unless the contract permits selling above the envelope." />
              </FieldGrid>
            </FormSection>
            <FormSection title="Contractual liability" description="What we owe whether or not we draw the capacity. These fields are what make idle capacity worth alerting on.">
              <FieldGrid>
                <TextField control={form.control} name="minimumSpend" label="Minimum spend" inputMode="decimal" prefix="$" />
                <TextField control={form.control} name="takeOrPayFloorPct" label="Take-or-pay floor (%)" inputMode="decimal" />
                <TextField control={form.control} name="prepaidPct" label="Prepaid share (%)" inputMode="decimal" />
                <TextField control={form.control} name="prepaidAmount" label="Prepaid amount" inputMode="decimal" prefix="$" />
                <TextField control={form.control} name="noticeDays" label="Notice to exit (days)" inputMode="numeric" />
                <SwitchField control={form.control} name="isAutoRenew" label="Auto-renews" description="Renewal alerting depends on this being honest." />
                <TextAreaField control={form.control} name="notes" label="Commercial notes" className="sm:col-span-2" description="Caveats a seller would need before promising this capacity." />
              </FieldGrid>
            </FormSection>
          </SheetBody>
          <SheetActions pending={save.isPending} disabled={!writable} onCancel={() => onOpenChange(false)} label="Record commitment" />
        </form>
      </Form>
    </RecordSheet>
  );
}

function activityDefaults(accountId?: string): ActivityForm {
  return {
    type: 'call', accountId: accountId ?? '', relatedDeal: '', contactId: '', subject: '', body: '',
    occurredAt: localDateTimeValue(new Date()),
  };
}

/**
 * The envelope `POST /api/activities` answers with. A synced event that was
 * already logged is a success with nothing inserted, so the row can be null —
 * and the field is optional here because the client must not break if that
 * response shape is ever tightened.
 */
interface LoggedActivity {
  activity: { id: string } | null;
  deduplicated?: boolean;
}

/**
 * Logging what a person did.
 *
 * Every UI mutation already writes a derived note, so the timeline is never
 * frozen — but calls, meetings and emails are the entries a human recognises,
 * and until this existed they could only be written over MCP. `activity:write`
 * is held per team and checked twice on the server: once for the principal, and
 * once against the side of the account named here, which is why the account is
 * required rather than optional.
 */
export function LogActivitySheet({ open, onOpenChange, identity, defaultAccountId, defaultContactId }: CreateSheetProps & { defaultContactId?: string }) {
  const queryClient = useQueryClient();
  const { data: accountData } = useQuery({ queryKey: ['accounts', 'activity-record-options'], queryFn: () => get<AccountRecord[]>('/api/accounts'), enabled: open });
  const { data: contactRows } = useQuery({ queryKey: ['contacts', 'activity-record-options'], queryFn: () => get<ContactRow[]>('/api/contacts'), enabled: open });
  const { data: demandBoard } = useQuery({ queryKey: ['/api/deals/demand'], queryFn: () => get<{ deals: { deal: DemandDealRecord }[] }>('/api/deals/demand'), enabled: open });
  const { data: supplyBoard } = useQuery({ queryKey: ['/api/deals/supply'], queryFn: () => get<{ deals: { deal: SupplyDealRecord }[] }>('/api/deals/supply'), enabled: open });
  const form = useForm<ActivityForm>({ resolver: zodResolver(activityFormSchema), defaultValues: activityDefaults(defaultAccountId) });
  useEffect(() => {
    // Reset on open rather than on mount so the timestamp is the moment the
    // sheet was opened, not the moment the page was first rendered.
    if (open) form.reset({ ...activityDefaults(defaultAccountId), contactId: defaultContactId ?? '' });
  }, [defaultAccountId, defaultContactId, form, open]);

  const loggableAccounts = useMemo(() => (accountData ?? []).filter((account) => canLogAgainstSide(identity, account.side)), [accountData, identity]);
  const accountId = form.watch('accountId');
  const type = form.watch('type');
  // Two questions, and both have to be asked. `canAny` covers the person who
  // holds the capability nowhere, whose account list is empty and who would
  // otherwise see an enabled button before choosing anything; the membership
  // test covers the account whose side they cannot write.
  const permitted = canAny(identity, 'activity:write');
  const writable =
    permitted && (accountId === '' || loggableAccounts.some((account) => account.id === accountId));
  const contactOptions = (contactRows ?? []).filter((row) => row.contact.accountId === accountId);
  const dealOptions = [
    ...(demandBoard?.deals ?? []).filter((row) => row.deal.accountId === accountId).map((row) => ({ value: `demand:${row.deal.id}`, label: `${row.deal.name} · demand` })),
    ...(supplyBoard?.deals ?? []).filter((row) => row.deal.accountId === accountId).map((row) => ({ value: `supply:${row.deal.id}`, label: `${row.deal.name} · supply` })),
  ];

  const save = useMutation({
    mutationFn: (values: ActivityForm) =>
      post<LoggedActivity>('/api/activities', {
        type: values.type,
        accountId: values.accountId,
        // Omitted rather than null: the endpoint accepts these keys only as
        // UUIDs, and a null would be rejected as an invalid activity.
        contactId: values.contactId || undefined,
        subject: values.subject.trim(),
        body: values.body.trim() || undefined,
        occurredAt: new Date(values.occurredAt).toISOString(),
        ...dealReference(values.relatedDeal),
      }),
    onSuccess: async (result, values) => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ['dashboard'] }),
        queryClient.invalidateQueries({ queryKey: ['accounts'] }),
        queryClient.invalidateQueries({ queryKey: ['contacts'] }),
        queryClient.invalidateQueries({ queryKey: ['growth'] }),
      ]);
      if (result?.deduplicated) {
        toast.success('Already on the timeline', { description: 'An identical event had already been synced, so nothing was added.' });
      } else {
        toast.success(`${label(values.type)} logged`, { description: 'It is on the account timeline and has moved the last-activity date.' });
      }
      onOpenChange(false);
    },
    onError: (error) => toast.error(errorMessage(error)),
  });

  return (
    <RecordSheet open={open} onOpenChange={onOpenChange} category="Timeline" title="Log activity" description="What actually happened with a counterparty. Record changes write their own notes; this is for the conversation behind them.">
      <Form {...form}>
        <form className="flex min-h-0 flex-1 flex-col" onSubmit={form.handleSubmit((values) => save.mutate(values))}>
          <SheetBody>
            {writable ? null : (
              <PermissionNotice>
                {permitted
                  ? 'Logging activity against this account needs write access to its side of the book. Choose another account, or ask a platform administrator.'
                  : 'Logging activity needs write access to one side of the book. A platform administrator can grant it.'}
              </PermissionNotice>
            )}
            <FieldGrid>
              <SelectField control={form.control} name="type" label="Type" options={LOGGABLE_ACTIVITY_TYPES.map((value) => ({ value, label: label(value) }))} />
              <TextField control={form.control} name="occurredAt" label="Happened at" type="datetime-local" />
              <SelectField control={form.control} name="accountId" label="Account" className="sm:col-span-2" options={loggableAccounts.map((account) => ({ value: account.id, label: `${account.name} · ${label(account.side)}` }))} />
              <TextField control={form.control} name="subject" label="Subject" placeholder="Pricing call on the Q4 renewal" className="sm:col-span-2" />
              <TextAreaField control={form.control} name="body" label="Detail" description={ACTIVITY_TYPE_HINTS[type]} className="sm:col-span-2" />
            </FieldGrid>
            <FormSection title="What it was about" description="Optional, and worth setting: a call attached to a deal and a person is the difference between a timeline and a diary.">
              <FieldGrid>
                <SelectField control={form.control} name="relatedDeal" label="Related deal" optional options={dealOptions} />
                <SelectField control={form.control} name="contactId" label="Contact involved" optional options={contactOptions.map((row) => ({ value: row.contact.id, label: `${row.contact.fullName}${row.contact.title ? ` · ${row.contact.title}` : ''}` }))} />
              </FieldGrid>
            </FormSection>
          </SheetBody>
          <SheetActions pending={save.isPending} disabled={!writable} onCancel={() => onOpenChange(false)} label="Log activity" />
        </form>
      </Form>
    </RecordSheet>
  );
}

function RecordSheet({ open, onOpenChange, category, title, description, children }: { open: boolean; onOpenChange(open: boolean): void; category: string; title: string; description: string; children: React.ReactNode }) {
  return (
    <Sheet open={open} onOpenChange={onOpenChange}>
      <SheetContent className="flex h-full w-full flex-col gap-0 overflow-hidden border-border p-0 sm:max-w-xl">
        {/*
          `band` is the primitive's standard top band — the overlay header
          padding, the rule against the body and the top safe-area inset, which
          this sheet was hand-rolling at its own values. `pr-14` stays: the
          dismiss control is absolutely positioned over this row, and a long
          title running under it is the one thing the band cannot know about.
        */}
        <SheetHeader band className="pr-14">
          <Badge className="mb-1 w-fit" tone="neutral">{category}</Badge>
          <SheetTitle>{title}</SheetTitle>
          <SheetDescription>{description}</SheetDescription>
        </SheetHeader>
        {children}
      </SheetContent>
    </Sheet>
  );
}

function SheetBody({ children }: { children: React.ReactNode }) {
  return <div className="flex min-h-0 flex-1 flex-col gap-7 overflow-y-auto overscroll-contain px-5 py-5 sm:px-6">{children}</div>;
}

function SheetActions({ pending, onCancel, label: actionLabel, disabled = false }: { pending: boolean; onCancel(): void; label: string; disabled?: boolean }) {
  return (
    <>
      <Separator />
      <div className="flex shrink-0 flex-col-reverse gap-2 px-5 pb-[calc(1rem+var(--safe-bottom))] pt-4 sm:flex-row sm:justify-end sm:px-6">
        <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button>
        <Button type="submit" disabled={pending || disabled}>
          {pending ? <LoaderCircle data-icon="inline-start" className="animate-spin" aria-hidden /> : null}
          {pending ? 'Saving…' : actionLabel}
        </Button>
      </div>
    </>
  );
}

/**
 * Shown inside a sheet the caller should not have been able to open. The page
 * gates the affordance; this exists so that a deep link, or a permission
 * revoked while the tab was idle, explains itself instead of answering 403 on
 * submit.
 */
function PermissionNotice({ children }: { children: React.ReactNode }) {
  return (
    <div role="status" className="flex gap-3 rounded-md bg-surface-2 p-4 text-sm text-muted">
      <Lock className="mt-0.5 size-4 shrink-0" aria-hidden />
      <p>{children}</p>
    </div>
  );
}

function FieldGrid({ children }: { children: React.ReactNode }) {
  return <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">{children}</div>;
}

/**
 * A titled group of fields inside a record sheet.
 *
 * The heading, its size and its description are the primitive's now. This was
 * a 14px/600 heading over a 12px `text-muted-foreground` note — a sixth
 * section heading, and the only place in the product still reaching for
 * shadcn's colour name — so a sheet's groups read a size smaller than the
 * panels on the page behind it. The rule and the top inset stay here: they
 * separate one group from the next, which is this sheet's layout, not the
 * heading's job.
 */
function FormSection({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
  return (
    <div className="border-t border-border pt-6">
      <Section title={title} description={description}>{children}</Section>
    </div>
  );
}

function TextField<T extends FieldValues>({ control, name, label: fieldLabel, description, className, prefix, ...inputProps }: { control: Control<T>; name: FieldPath<T>; label: string; description?: string; className?: string; prefix?: string } & Omit<React.ComponentProps<typeof Input>, 'name' | 'value' | 'defaultValue'>) {
  return (
    <FormField control={control} name={name} render={({ field }) => (
      <FormItem className={className}>
        <FormLabel>{fieldLabel}</FormLabel>
        <FormControl><Input {...field} {...inputProps} value={String(field.value ?? '')} placeholder={inputProps.placeholder ?? prefix} /></FormControl>
        {description ? <FormDescription>{description}</FormDescription> : null}
        <FormMessage />
      </FormItem>
    )} />
  );
}

function TextAreaField<T extends FieldValues>({ control, name, label: fieldLabel, description, className, placeholder }: { control: Control<T>; name: FieldPath<T>; label: string; description?: string; className?: string; placeholder?: string }) {
  return (
    <FormField control={control} name={name} render={({ field }) => (
      <FormItem className={className}>
        <FormLabel>{fieldLabel}</FormLabel>
        <FormControl><Textarea {...field} value={String(field.value ?? '')} placeholder={placeholder} className="min-h-24 resize-y" /></FormControl>
        {description ? <FormDescription>{description}</FormDescription> : null}
        <FormMessage />
      </FormItem>
    )} />
  );
}

function SelectField<T extends FieldValues>({ control, name, label: fieldLabel, options, optional = false, className }: { control: Control<T>; name: FieldPath<T>; label: string; options: { value: string; label: string; disabled?: boolean }[]; optional?: boolean; className?: string }) {
  return (
    <FormField control={control} name={name} render={({ field }) => (
      <FormItem className={className}>
        <FormLabel>{fieldLabel}</FormLabel>
        <Select value={String(field.value || (optional ? 'none' : ''))} onValueChange={(value) => field.onChange(value === 'none' ? '' : value)}>
          <FormControl><SelectTrigger><SelectValue placeholder={`Select ${fieldLabel.toLowerCase()}`} /></SelectTrigger></FormControl>
          <SelectContent><SelectGroup>
            {optional ? <SelectItem value="none">None</SelectItem> : null}
            {options.map((option) => <SelectItem key={option.value} value={option.value} disabled={option.disabled}>{option.label}</SelectItem>)}
          </SelectGroup></SelectContent>
        </Select>
        <FormMessage />
      </FormItem>
    )} />
  );
}

function SwitchField<T extends FieldValues>({ control, name, label: fieldLabel, description }: { control: Control<T>; name: FieldPath<T>; label: string; description?: string }) {
  return (
    <FormField control={control} name={name} render={({ field }) => (
      <FormItem className="flex min-h-20 flex-row items-center justify-between gap-4 rounded-lg border border-border p-3">
        <div className="flex flex-col gap-1"><FormLabel>{fieldLabel}</FormLabel>{description ? <FormDescription>{description}</FormDescription> : null}</div>
        <FormControl><Switch checked={Boolean(field.value)} onCheckedChange={field.onChange} /></FormControl>
      </FormItem>
    )} />
  );
}

function errorMessage(error: unknown): string {
  if (error instanceof ApiError) return error.message;
  return error instanceof Error ? error.message : 'The record could not be saved.';
}
