import clsx from 'clsx'
import {
  EDUCATION_CHOICES,
  INVERTED_DIET_CHOICES,
  INVERTED_EDUCATION_CHOICES,
  INVERTED_GENDERS,
  INVERTED_LANGUAGE_CHOICES,
  INVERTED_MBTI_CHOICES,
  INVERTED_ORIENTATION_CHOICES,
  INVERTED_POLITICAL_CHOICES,
  INVERTED_RACE_CHOICES,
  INVERTED_RELATIONSHIP_CHOICES,
  INVERTED_RELATIONSHIP_STATUS_CHOICES,
  INVERTED_RELIGION_CHOICES,
  MBTI_TYPE_NAMES,
} from 'common/choices'
import {DemographicField, Distribution} from 'common/stats'
import {ComponentType, SVGProps} from 'react'
import {surface} from 'web/components/widgets/surface'

/**
 * A single profile-field breakdown as a ranked bar list — the building block of the "Who's on Compass"
 * section of /stats. Deliberately the same bar vocabulary as `country-spread.tsx`: a member scanning the
 * page reads one distribution the same way as the next, so education, religion and languages all share a
 * form and only the labels change.
 *
 * The raw stored values (`'bachelors'`, `'veg'`, `'intj'`) are turned into human labels here rather than
 * on the wire, so the payload stays small and the label maps live next to the rest of the choice tables.
 *
 * Percentages are always of `dist.base` — the members who answered the field — not of the whole platform.
 * For a multi-select field that base is the distinct answerers, so a member who picked three diets counts
 * once in the denominator; that is why the multi-select bars can each be large yet not sum to 100%, and
 * why the subtitle says so out loud.
 */

type IconType = ComponentType<SVGProps<SVGSVGElement>>

// value → English label, per field. Missing fields (age buckets are already human-readable) fall through
// to the raw value. Same maps the search filters use, so a label never disagrees between the two places.
const LABELS: Partial<Record<DemographicField, Record<string, string>>> = {
  gender: INVERTED_GENDERS,
  education_level: INVERTED_EDUCATION_CHOICES,
  political_beliefs: INVERTED_POLITICAL_CHOICES,
  religion: INVERTED_RELIGION_CHOICES,
  diet: INVERTED_DIET_CHOICES,
  ethnicity: INVERTED_RACE_CHOICES,
  orientation: INVERTED_ORIENTATION_CHOICES,
  pref_relation_styles: INVERTED_RELATIONSHIP_CHOICES,
  relationship_status: INVERTED_RELATIONSHIP_STATUS_CHOICES,
  languages: INVERTED_LANGUAGE_CHOICES,
}

function capitalize(s: string) {
  return s.charAt(0).toUpperCase() + s.slice(1)
}

// Education reads best in level order rather than by popularity — the levels are an ordinal ladder, so
// showing PhD above High school (the choices dict, reversed) is clearer than "whichever is most common
// first". Everything else keeps the count ranking the backend already sorted it into.
const EDUCATION_ORDER: string[] = Object.values(EDUCATION_CHOICES).reverse()

function orderItems(field: DemographicField, items: Distribution['items']): Distribution['items'] {
  if (field !== 'education_level') return items
  return [...items].sort(
    (a, b) => EDUCATION_ORDER.indexOf(a.value) - EDUCATION_ORDER.indexOf(b.value),
  )
}

// Exported so the home and about pages label the same raw values the same way this card does — they name a
// few of these distributions in prose, and a "Bachelor's" here against a "bachelors" there would be the
// kind of drift the shared choices maps exist to prevent.
export function labelFor(field: DemographicField, value: string): string {
  // MBTI reads as the four-letter code plus its archetype — "INTJ" alone is jargon; "INTJ Architect"
  // tells the reader what it means without a second lookup.
  if (field === 'mbti') {
    const code = INVERTED_MBTI_CHOICES[value] ?? value.toUpperCase()
    const name = MBTI_TYPE_NAMES[code]
    return name ? `${code} · ${name}` : code
  }
  return LABELS[field]?.[value] ?? capitalize(value)
}

// Exported so the compact mini-charts on the home and about pages (a single field's top values, with no
// surrounding card) draw the exact same bar as this one's own rows — one bar vocabulary everywhere a count
// is shown as a fraction of a base, rather than a second one invented per page. `rank` defaults to 0
// (no fade) for callers whose bar width is already the true percentage rather than normalised to a leader,
// where the fade's job of distinguishing near-equal bars doesn't apply.
export function DistRow({
  label,
  pct,
  widthPct,
  rank = 0,
}: {
  label: string
  pct: number
  widthPct: number
  rank?: number
}) {
  return (
    <div className="flex items-center gap-3">
      <div className="w-24 shrink-0 truncate text-[13px] text-ink-700 sm:w-28" title={label}>
        {label}
      </div>
      <div className="h-2 flex-1 overflow-hidden rounded-full bg-canvas-200">
        <div
          className="h-full rounded-full bg-primary-500 transition-[width] duration-700 ease-out"
          // The bar shortens the ranking gently down the list — the top value stays full-strength amber
          // and each lower row loses a little, so the order reads even where two counts are close.
          style={{width: `${widthPct}%`, opacity: Math.max(0.45, 1 - rank * 0.11)}}
        />
      </div>
      <div className="w-9 shrink-0 text-right text-xs tabular-nums text-ink-500">{pct}%</div>
    </div>
  )
}

export function DistributionCard({
  field,
  title,
  icon: Icon,
  dist,
}: {
  field: DemographicField
  title: string
  icon: IconType
  dist: Distribution | undefined
}) {
  if (!dist || !dist.items.length) return null

  // Most fields arrive count-sorted from the backend; education is re-laid into level order here.
  const items = orderItems(field, dist.items)

  // Bar widths are relative to the largest bar, not to the base: against the base a field where the top
  // answer is only 20% would render as a row of stubs and the ranking would be unreadable. Percentages
  // (the number on the right) are still of the base — only the drawn width is normalised to the leader.
  // Taken as the true max, not items[0], because age buckets (and education) aren't ordered by size.
  const max = Math.max(...items.map((it) => it.count))

  return (
    <div className={clsx(surface, 'flex h-full flex-col p-5 sm:p-6')}>
      <div className="mb-4 flex items-center gap-3">
        <div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-primary-100 ring-1 ring-primary-200">
          <Icon className="h-[18px] w-[18px] text-primary-600" strokeWidth={1.8} />
        </div>
        <div className="min-w-0">
          <h3 className="text-sm font-bold leading-tight text-ink-900">{title}</h3>
          <p className="text-xs text-ink-500">
            {dist.multi
              ? `of ${dist.base.toLocaleString()} who shared`
              : `${dist.base.toLocaleString()} shared`}
          </p>
        </div>
      </div>

      <div className="flex flex-col gap-2.5">
        {items.map((it, i) => (
          <DistRow
            key={it.value}
            label={labelFor(field, it.value)}
            pct={Math.max(1, Math.round((it.count / dist.base) * 100))}
            widthPct={Math.max(4, Math.round((it.count / max) * 100))}
            rank={i}
          />
        ))}
      </div>
    </div>
  )
}
