/**
 * Scoring a deal against a qualification framework.
 *
 * The arithmetic is `motionScoreBasisPoints` from `@pig/core` — the same pure
 * function the API calls before it writes the row. That import is the whole
 * design of this component: a client that computes the total itself would
 * eventually disagree with the server about a number people act on, and a
 * qualification score that reads 74% while you are filling the form in and
 * 71% once it is saved is worse than one the form never showed at all.
 * Nothing here posts a score; it posts the dimensions and lets the server
 * recompute, so the two can never diverge even if this file is wrong.
 *
 * Every dimension must be answered before Save enables. A partial score is not
 * a smaller score — the maximum shrinks with it, so answering only the three
 * dimensions that went well produces a band of "Strategic" on a deal nobody
 * has qualified. The running total shown while the form is incomplete is
 * labelled provisional for exactly that reason.
 *
 * `fields` is authored JSON and arrives unvalidated, the same way it does in
 * `FieldsView`: a dimension without an id cannot be posted (the API keys on
 * it) and a dimension without anchors cannot be scored honestly, so both are
 * dropped rather than rendered as an empty control.
 */
import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ArrowDownRight, ArrowUpRight, Minus } from 'lucide-react';
import { toast } from 'sonner';
import {
  MOTION_MAX_DIMENSION_SCORE,
  MOTION_MIN_DIMENSION_SCORE,
  motionBand,
  motionScoreBasisPoints,
  type MotionDimensionScore,
} from '@pig/core';
import { post } from '@/lib/api';
import { Badge, Button, cn } from '@/components/ui';
import { Textarea } from '@/components/ui/textarea';
import { anchorScale, asRecord, number, text, type AnchorRow } from './fields';
import { percent } from './format';

/** A dimension this component was able to make sense of. */
interface ScorableDimension {
  id: string;
  name: string;
  weight: number;
  group: string | null;
  why: string | null;
  anchors: AnchorRow[];
}

export function QualificationScorer({
  engagementId,
  frameworkTemplateId,
  fields,
  previousBasisPoints = null,
  onScored,
  onCancel,
}: {
  engagementId: string;
  /** Recorded on the score so the framework behind it stays identifiable. */
  frameworkTemplateId: string | null;
  /** The framework's `fields`. Untrusted author JSON — see the header. */
  fields: unknown;
  /** The last score on this engagement, if there is one, for the movement. */
  previousBasisPoints?: number | null;
  onScored: () => void;
  onCancel: () => void;
}) {
  const queryClient = useQueryClient();
  const dimensions = useMemo(() => scorableDimensions(fields), [fields]);
  const [answers, setAnswers] = useState<Record<string, number>>({});
  const [note, setNote] = useState('');

  const answered: MotionDimensionScore[] = dimensions
    .filter((dimension) => answers[dimension.id] !== undefined)
    .map((dimension) => ({
      id: dimension.id,
      weight: dimension.weight,
      score: answers[dimension.id] as number,
    }));
  const complete = dimensions.length > 0 && answered.length === dimensions.length;
  const basisPoints = motionScoreBasisPoints(answered);
  const band = motionBand(basisPoints);
  const movement =
    complete && previousBasisPoints !== null ? basisPoints - previousBasisPoints : null;

  const save = useMutation({
    mutationFn: () =>
      post<{ score: { id: string } }>(`/api/motion/engagements/${engagementId}/scores`, {
        dimensions: answered,
        frameworkTemplateId,
        note: note.trim() ? note.trim() : null,
      }),
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['motion'] });
      toast.success('Qualification scored');
      onScored();
    },
    onError: (error: Error) => toast.error(error.message),
  });

  if (dimensions.length === 0) {
    return (
      <div className="mt-6 rounded-lg border border-border p-4">
        <p className="font-medium">This framework has no scorable dimensions</p>
        <p className="mt-1 text-sm text-muted">
          A dimension needs an id, a name and at least one anchor before it can be scored. Fix
          the framework in the library, then score against a new version of it.
        </p>
        <Button className="mt-4" type="button" variant="outline" onClick={onCancel}>
          Close
        </Button>
      </div>
    );
  }

  return (
    <form
      className="mt-5 flex min-w-0 flex-col gap-5 pb-6"
      onSubmit={(event) => {
        event.preventDefault();
        save.mutate();
      }}
    >
      {/* Sticky, because the reason to show a running total at all is to let
          somebody see a marginal answer move the band while they are still
          looking at the anchors that produced it. */}
      <div className="sticky top-0 z-10 -mx-1 min-w-0 rounded-xl border border-border bg-surface p-4">
        <div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1">
          <span className="nums text-3xl font-semibold leading-none">{percent(basisPoints)}</span>
          <Badge tone={band.tone}>{band.label}</Badge>
          {movement === null ? null : <Movement delta={movement} />}
        </div>
        <p className="mt-2 text-xs text-muted">
          {complete ? (
            <>
              Weighted across {dimensions.length} dimensions ·{' '}
              <span className="nums">{basisPoints}</span> basis points
            </>
          ) : (
            <>
              Provisional — {answered.length} of {dimensions.length} scored. The maximum shrinks
              with the dimensions you leave blank, so this band is not the deal's band yet.
            </>
          )}
        </p>
      </div>

      <div className="min-w-0 space-y-4">
        {dimensions.map((dimension) => (
          <fieldset key={dimension.id} className="min-w-0 rounded-xl border border-border p-3">
            <legend className="flex min-w-0 flex-wrap items-center gap-2 px-1">
              <span className="min-w-0 break-words font-medium leading-6">{dimension.name}</span>
              {dimension.group ? <Badge tone="neutral">{dimension.group}</Badge> : null}
              <span className="nums whitespace-nowrap text-xs text-muted">
                {dimension.weight === 0 ? 'no weight' : `weight ${dimension.weight}`}
              </span>
            </legend>
            {dimension.why ? (
              <p className="min-w-0 break-words text-sm leading-6 text-muted">{dimension.why}</p>
            ) : null}
            <div className="mt-2 min-w-0 space-y-1.5">
              {dimension.anchors.map((anchor) => {
                const selected = answers[dimension.id] === anchor.score;
                return (
                  <label
                    key={anchor.score}
                    className={cn(
                      'tap flex min-h-11 min-w-0 cursor-pointer gap-3 rounded-lg border p-2.5',
                      selected ? 'border-brand bg-surface-2' : 'border-border hover:bg-surface-2',
                    )}
                  >
                    <input
                      type="radio"
                      className="sr-only"
                      name={`dimension-${dimension.id}`}
                      value={anchor.score}
                      checked={selected}
                      onChange={() =>
                        setAnswers((current) => ({ ...current, [dimension.id]: anchor.score }))
                      }
                    />
                    <span
                      className={cn(
                        'nums flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold',
                        selected ? 'bg-primary text-primary-foreground' : 'bg-surface-2 text-muted',
                      )}
                      aria-hidden
                    >
                      {anchor.score}
                    </span>
                    <span className="min-w-0 break-words text-sm leading-6">{anchor.anchor}</span>
                  </label>
                );
              })}
            </div>
          </fieldset>
        ))}
      </div>

      <div className="flex min-w-0 flex-col gap-1.5">
        <label className="text-sm font-medium" htmlFor="qualification-note">
          What moved since the last score
        </label>
        <Textarea
          id="qualification-note"
          className="min-h-24"
          value={note}
          onChange={(event) => setNote(event.target.value)}
          placeholder="The evidence behind the answers that changed."
        />
      </div>

      {save.isError ? <p className="text-sm text-danger">{save.error.message}</p> : null}

      <div className="flex min-w-0 flex-wrap justify-end gap-2">
        <Button type="button" variant="ghost" onClick={onCancel}>
          Cancel
        </Button>
        <Button
          type="submit"
          variant="primary"
          disabled={!complete || save.isPending}
          title={complete ? undefined : 'Score every dimension first.'}
        >
          Record score
        </Button>
      </div>
    </form>
  );
}

function Movement({ delta }: { delta: number }) {
  if (delta === 0) {
    return (
      <span className="inline-flex items-center gap-1 text-xs text-muted">
        <Minus className="size-3.5" aria-hidden />
        Unchanged
      </span>
    );
  }
  const Icon = delta > 0 ? ArrowUpRight : ArrowDownRight;
  return (
    <span
      className={cn(
        'nums inline-flex items-center gap-1 text-xs font-medium',
        delta > 0 ? 'text-positive' : 'text-danger',
      )}
    >
      <Icon className="size-3.5" aria-hidden />
      {delta > 0 ? '+' : '−'}
      {percent(Math.abs(delta))}
    </span>
  );
}

// ------------------------------------------------------------------ narrowing

function scorableDimensions(fields: unknown): ScorableDimension[] {
  const record = asRecord(fields);
  const raw = Array.isArray(record?.dimensions) ? record.dimensions : [];
  const seen = new Set<string>();
  const scorable: ScorableDimension[] = [];

  for (const entry of raw) {
    const dimension = asRecord(entry);
    if (!dimension) continue;
    const id = text(dimension.id);
    const name = text(dimension.name);
    if (!id || !name || seen.has(id)) continue;
    const anchors = anchorScale(dimension.anchors);
    if (anchors.length === 0) continue;
    seen.add(id);
    scorable.push({
      id,
      name,
      // Clamped to the API's own bound rather than trusted: a weight the seed
      // author typed as 1500 would otherwise fail zod after the whole form is
      // filled in, which reads as the save being broken.
      weight: Math.min(1_000, Math.max(0, Math.round(number(dimension.weight) ?? 0))),
      group: text(dimension.group),
      why: text(dimension.why),
      anchors,
    });
  }
  return scorable;
}
