/**
 * Motion — the reusable practice that sits alongside the ledger.
 *
 * PIG already models what capacity was bought and what was sold. It does not
 * model the *motion*: the repeatable work that turns a messy customer
 * conversation into a scoped deployment, and turns that deployment into an
 * asset the next one reuses. `DEMAND_STAGES` is already exactly that sequence,
 * so Motion is deliberately NOT a parallel entity tree — every kind below
 * declares which demand stages it serves, and the artifacts hang off deals that
 * already exist.
 *
 * The loop the whole feature exists for:
 *
 *   library template --instantiate--> engagement artifact --promote--> template v2
 *
 * A template is never edited in place once it has been used; promotion writes a
 * new version row pointing back at its predecessor and at the artifact that
 * proved it. That is what makes "each deployment makes the next one easier" a
 * mechanism rather than a slogan, and it is why `usageCount` and the lineage
 * columns are load-bearing rather than decoration.
 *
 * No zod in this file. `packages/core` is the ontology layer; the API route
 * modules build their schemas from these constants, so removing a value stops
 * validating rather than silently persisting.
 */
import { DEMAND_OPEN_STAGES, type DemandStage } from './ontology';

// ---------------------------------------------------------------------------
// The nine kinds
// ---------------------------------------------------------------------------

export const MOTION_KINDS = [
  'discovery',
  'qualification',
  'poc',
  'proposal',
  'pricing',
  'architecture',
  'case_study',
  'narrative',
  'playbook',
] as const;
export type MotionKind = (typeof MOTION_KINDS)[number];

export const MOTION_KIND_LABELS: Record<MotionKind, string> = {
  discovery: 'Discovery',
  qualification: 'Qualification framework',
  poc: 'POC structure',
  proposal: 'Proposal blocks',
  pricing: 'Pricing and packaging',
  architecture: 'Reference architecture',
  case_study: 'Case study',
  narrative: 'Technical narrative',
  playbook: 'Deployment playbook',
};

/**
 * One line each, in the register of `ontology.ts`: what the kind is *for*, not
 * what it contains. These strings are shown in the library filter and in
 * Piggy's tool description, so they are the only definition most people read.
 */
export const MOTION_KIND_DESCRIPTIONS: Record<MotionKind, string> = {
  discovery: 'The questions that surface what a customer is actually training, before anyone scopes it.',
  qualification: 'Weighted dimensions that turn a judgement about a deal into a score somebody can argue with.',
  poc: 'What a proof of concept must demonstrate, and what closes it — so a pilot cannot run forever.',
  proposal: 'Language blocks assembled into a proposal, so wording that survived procurement is reused.',
  pricing: 'The inputs behind a quoted price: term, commitment shape, and what the block cost.',
  architecture: 'A deployment shape that has already worked, described well enough to be copied.',
  case_study: 'A deployment written up as evidence, for the next customer who asks whether this is real.',
  narrative: 'The technical argument for why this capacity suits this workload, written once.',
  playbook: 'The end-to-end sequence for a deployment, spanning every stage rather than one.',
};

/**
 * Which demand stages each kind serves.
 *
 * The closed stages are absent everywhere on purpose: a deal that is won or
 * lost has left the motion, and offering to instantiate a discovery template
 * into it would be an invitation to file work against a dead deal. `playbook`
 * spans the whole live motion, which is what distinguishes it from the eight
 * kinds that answer one stage.
 */
export const MOTION_KIND_STAGES: Record<MotionKind, readonly DemandStage[]> = {
  discovery: ['qualification', 'scoping'],
  qualification: ['qualification'],
  poc: ['poc'],
  proposal: ['proposal', 'procurement'],
  pricing: ['proposal', 'procurement'],
  architecture: ['scoping', 'poc', 'deployment'],
  case_study: ['qualification', 'proposal', 'expansion'],
  narrative: ['qualification', 'scoping', 'proposal'],
  playbook: DEMAND_OPEN_STAGES,
};

/**
 * Private is the default, and it is a real access rule rather than a label —
 * see the header of `packages/db/src/schema/motion.ts`. A library nobody can
 * draft in privately becomes a library nobody drafts in.
 */
export const MOTION_VISIBILITIES = ['private', 'shared'] as const;
export type MotionVisibility = (typeof MOTION_VISIBILITIES)[number];

export const MOTION_VISIBILITY_LABELS: Record<MotionVisibility, string> = {
  private: 'Private',
  shared: 'Shared',
};

/** Only a `final` artifact may be promoted — see the promotion path in the API. */
export const ARTIFACT_STATUSES = ['draft', 'review', 'final'] as const;
export type ArtifactStatus = (typeof ARTIFACT_STATUSES)[number];

export const ARTIFACT_STATUS_LABELS: Record<ArtifactStatus, string> = {
  draft: 'Draft',
  review: 'In review',
  final: 'Final',
};

export const ENGAGEMENT_STATUSES = ['open', 'won', 'lost', 'paused'] as const;
export type EngagementStatus = (typeof ENGAGEMENT_STATUSES)[number];

export const ENGAGEMENT_STATUS_LABELS: Record<EngagementStatus, string> = {
  open: 'Open',
  won: 'Won',
  lost: 'Lost',
  paused: 'Paused',
};

export function isMotionKind(value: string): value is MotionKind {
  return (MOTION_KINDS as readonly string[]).includes(value);
}

export function isMotionVisibility(value: string): value is MotionVisibility {
  return (MOTION_VISIBILITIES as readonly string[]).includes(value);
}

export function isArtifactStatus(value: string): value is ArtifactStatus {
  return (ARTIFACT_STATUSES as readonly string[]).includes(value);
}

export function isEngagementStatus(value: string): value is EngagementStatus {
  return (ENGAGEMENT_STATUSES as readonly string[]).includes(value);
}

// ---------------------------------------------------------------------------
// Scoring
// ---------------------------------------------------------------------------

/** Each dimension is scored on this scale, inclusive. */
export const MOTION_MIN_DIMENSION_SCORE = 0;
export const MOTION_MAX_DIMENSION_SCORE = 4;

/** The full scale of a motion score. Basis points, exactly as money is cents. */
export const MOTION_BASIS_POINTS_MAX = 10_000;

export interface MotionDimensionScore {
  readonly id: string;
  readonly weight: number;
  readonly score: number;
}

/**
 * Weighted score in basis points of the maximum (0–10000). Weights need not
 * sum to 100.
 *
 * Basis points rather than a float for the same reason money is cents: a score
 * decides what a seller is told to do, it is persisted, and 0.55 is not 0.55 in
 * binary. The whole computation stays in integers and rounds half-up at the
 * single division, so the stored number is the number that was displayed.
 *
 * Inputs are normalised rather than rejected — a dimension carrying a
 * non-finite weight is a bug upstream, and throwing here would take a
 * dashboard down rather than under-weight one row.
 */
export function motionScoreBasisPoints(dimensions: readonly MotionDimensionScore[]): number {
  let weightTotal = 0;
  let weightedScoreTotal = 0;

  for (const dimension of dimensions) {
    if (!Number.isFinite(dimension.weight) || !Number.isFinite(dimension.score)) continue;
    const weight = Math.round(dimension.weight);
    if (weight <= 0) continue;
    const score = Math.min(
      MOTION_MAX_DIMENSION_SCORE,
      Math.max(MOTION_MIN_DIMENSION_SCORE, Math.round(dimension.score)),
    );
    weightTotal += weight;
    weightedScoreTotal += weight * score;
  }

  // No weight is not a zero score dressed up — it is "nothing was asked". Both
  // answer 0 here because a qualification with no dimensions has to render as
  // something, and NaN renders as `NaN`.
  if (weightTotal === 0) return 0;

  const numerator = MOTION_BASIS_POINTS_MAX * weightedScoreTotal;
  const denominator = MOTION_MAX_DIMENSION_SCORE * weightTotal;

  // Half-up, in integers. `Math.round` on the quotient would be a float
  // division first, which is exactly the step that loses the half.
  return Math.floor((2 * numerator + denominator) / (2 * denominator));
}

/*
 * The band labels are the SCORECARD'S OWN, not a second vocabulary invented
 * here. `seed/motion/trainability-qualification.json` publishes five bands and
 * an action for each, and a reader who has just read that document and then
 * sees the score rendered as "Strategic" has been told two different things
 * about the same number by the same product. Every label below is a verb the
 * reader can act on, which is the point of a band at all: "Qualified" describes
 * the deal, "Scope down" says what to do about it.
 *
 * Boundaries are the authored percentages in basis points. If the scorecard's
 * bands are ever re-authored, these move with them — they are one decision
 * recorded twice, and the JSON is the copy a customer reads.
 */
export const MOTION_BANDS = [
  { min: 0, max: 2999, label: 'Decline', tone: 'danger' },
  { min: 3000, max: 4599, label: 'Defer', tone: 'danger' },
  { min: 4600, max: 6199, label: 'Scope down', tone: 'warning' },
  { min: 6200, max: 7799, label: 'Qualified, conditional', tone: 'info' },
  { min: 7800, max: 10_000, label: 'Build', tone: 'positive' },
] as const;
export type MotionBand = (typeof MOTION_BANDS)[number];
export type MotionBandTone = MotionBand['tone'];

/**
 * The band a score falls in. Boundaries are inclusive at `min`, so 6200 is
 * "Qualified, conditional" and 6199 is "Scope down" — an off-by-one here
 * changes what a seller is told to do without changing any number they can
 * see.
 *
 * Out-of-range input clamps to an end band rather than returning undefined,
 * because every caller renders this and none of them has a null branch.
 */
export function motionBand(basisPoints: number): MotionBand {
  const clamped = Math.min(
    MOTION_BASIS_POINTS_MAX,
    Math.max(0, Number.isFinite(basisPoints) ? Math.round(basisPoints) : 0),
  );
  return MOTION_BANDS.find((band) => clamped >= band.min && clamped <= band.max) ?? MOTION_BANDS[0];
}
