import type { ComponentType } from 'react';
import { Button, FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material';
import type {
  InputId,
  OverrideDiagnostic,
  Preset,
  SelectionId,
  ThemeOverrideEditorDraft,
  ThemeSelections,
} from '../override/types';
import {
  diagnosticsForControl,
  type ControlDefinition,
  type EditorControlType,
  type ResolvedControlDefinition,
} from '../override/controls';
import { validateHex } from '../validation';
import { ColorControl } from './ColorControl';
import { FontControl } from './FontControl';
import { PresetControl } from './PresetControl';

type SelectionValue = NonNullable<ThemeSelections[SelectionId]>;

export interface ControlWidgetProps {
  readonly definition: ResolvedControlDefinition;
  readonly draft: ThemeOverrideEditorDraft;
  readonly diagnostics: readonly OverrideDiagnostic[];
  readonly fontOptions: Readonly<Record<Preset, readonly string[]>>;
  readonly onPresetChange: (preset: Preset) => void;
  readonly onInputChange: (inputId: InputId, value: string) => void;
  readonly onInputClear: (inputId: InputId) => void;
  readonly onSelectionChange: (
    definition: ResolvedControlDefinition,
    value: SelectionValue,
  ) => void;
  readonly onSelectionClear: (selectionId: SelectionId) => void;
}

function bindingError(definition: ControlDefinition, expected: string): never {
  throw new Error(
    `Editor control "${definition.id}" uses "${definition.binding.kind}" storage with a ${expected} widget.`,
  );
}

export function encodeSelectionReference(
  definition: ResolvedControlDefinition,
  reference: string,
): SelectionValue {
  if (definition.binding.kind !== 'selection') bindingError(definition, 'selection');
  const selection = definition.selectionDefinition;
  if (!selection) {
    throw new Error(`Editor control "${definition.id}" is missing resolved selection metadata.`);
  }

  if (selection.valueShape === 'invariant-reference') {
    if (selection.modePolicy !== 'invariant' || definition.modeExposure !== 'none') {
      throw new Error(
        `Editor control "${definition.id}" has incompatible invariant selection metadata.`,
      );
    }
    return { source: { ref: reference } };
  }

  if (selection.valueShape === 'linked-reference') {
    if (selection.modePolicy !== 'mode-capable' || definition.modeExposure === 'none') {
      throw new Error(
        `Editor control "${definition.id}" has incompatible linked selection metadata.`,
      );
    }
    return {
      mode: 'linked',
      source: { ref: reference },
    };
  }

  throw new Error(
    `Editor control "${definition.id}" cannot encode selection shape "${selection.valueShape}".`,
  );
}

function selectedReference(value: SelectionValue | undefined): string {
  return value && 'source' in value ? value.source.ref : '';
}

function PresetWidget({ definition, draft, onPresetChange }: Readonly<ControlWidgetProps>) {
  if (definition.binding.kind !== 'metadata') bindingError(definition, 'preset');
  return (
    <PresetControl
      id={definition.id}
      label={definition.label}
      value={draft.basePreset.displayValue}
      onChange={onPresetChange}
    />
  );
}

function FontWidget({
  definition,
  draft,
  fontOptions,
  onInputChange,
}: Readonly<ControlWidgetProps>) {
  if (definition.binding.kind !== 'input') bindingError(definition, 'font');
  const inputId = definition.binding.key;
  const field = draft.inputs[inputId];
  return (
    <FontControl
      id={definition.id}
      label={definition.label}
      options={fontOptions[draft.basePreset.displayValue]}
      value={field.displayValue}
      onChange={(value) => onInputChange(inputId, value)}
    />
  );
}

function HexInputWidget({
  definition,
  draft,
  diagnostics,
  onInputChange,
  onInputClear,
}: Readonly<ControlWidgetProps>) {
  if (definition.binding.kind !== 'input') bindingError(definition, 'input');
  const inputId = definition.binding.key;
  const field = draft.inputs[inputId];
  const relationshipLabel =
    definition.intentBehavior === 'pin-unpin' && field.relationship
      ? `${definition.label} (${field.relationship})`
      : definition.label;
  const diagnostic = diagnosticsForControl(definition, diagnostics).find(
    ({ severity }) => severity === 'error',
  );

  return (
    <>
      <ColorControl
        label={relationshipLabel}
        value={field.displayValue}
        error={diagnostic?.message ?? validateHex(field.displayValue)}
        onChange={(value) => onInputChange(inputId, value)}
      />
      {definition.intentBehavior === 'pin-unpin' && field.relationship === 'pinned' ? (
        <Button size="small" onClick={() => onInputClear(inputId)}>
          Use derived {definition.label}
        </Button>
      ) : null}
    </>
  );
}

function SelectionWidget({
  definition,
  draft,
  diagnostics,
  onSelectionChange,
  onSelectionClear,
}: Readonly<ControlWidgetProps>) {
  if (definition.binding.kind !== 'selection') bindingError(definition, 'selection');
  if (!definition.selectionDefinition || !definition.sourceSet) {
    throw new Error(`Editor control "${definition.id}" is missing resolved catalog metadata.`);
  }

  const { selectionId } = definition.binding;
  const value = selectedReference(draft.selections[selectionId]?.displayValue);
  const controlDiagnostics = diagnosticsForControl(definition, diagnostics);
  const error = controlDiagnostics.find(({ severity }) => severity === 'error');
  const warning = controlDiagnostics.find(({ severity }) => severity === 'warning');

  return (
    <FormControl size="small" error={Boolean(error)}>
      <InputLabel id={`${definition.id}-label`} shrink>
        {definition.label}
      </InputLabel>
      <Select
        labelId={`${definition.id}-label`}
        label={definition.label}
        value={value}
        displayEmpty
        renderValue={(selected) => selected || 'Preset default'}
        onChange={(event) => {
          const reference = event.target.value;
          if (reference) {
            onSelectionChange(definition, encodeSelectionReference(definition, reference));
          } else {
            onSelectionClear(selectionId);
          }
        }}
      >
        <MenuItem value="">Preset default</MenuItem>
        {definition.sourceSet.references.map((reference) => (
          <MenuItem key={reference} value={reference}>
            {reference}
          </MenuItem>
        ))}
      </Select>
      <FormHelperText>
        {error?.message ?? warning?.message ?? definition.description}
      </FormHelperText>
    </FormControl>
  );
}

type ControlWidget = ComponentType<Readonly<ControlWidgetProps>>;

const CONTROL_WIDGET_REGISTRY: Partial<Record<EditorControlType, ControlWidget>> = {
  'preset-select': PresetWidget,
  'hex-input': HexInputWidget,
  'font-select': FontWidget,
  'token-ref': SelectionWidget,
  'scale-ref': SelectionWidget,
};

export function resolveControlWidget(type: EditorControlType): ControlWidget {
  const widget = CONTROL_WIDGET_REGISTRY[type];
  if (!widget) {
    throw new Error(`No Theme Editor widget is registered for control type "${type}".`);
  }
  return widget;
}

export function validateControlWidgetRegistry(definitions: readonly ControlDefinition[]): void {
  for (const definition of definitions) {
    resolveControlWidget(definition.type);
  }
}

export function ControlRenderer(props: Readonly<ControlWidgetProps>) {
  const Widget = resolveControlWidget(props.definition.type);
  return <Widget {...props} />;
}
