'use client'

/**
 * Recursive input dispatcher for component-block props. Given a
 * {@link PropSpec} and the currently stored value, renders the correct
 * editor:
 *
 * | PropType.kind         | Editor                                         |
 * | --------------------- | ---------------------------------------------- |
 * | `string`              | single-line text input                         |
 * | `number`              | native number input                            |
 * | `boolean`             | switch (ToggleField look)                      |
 * | `enum`                | select with the literal values                 |
 * | `literal`             | readonly display (it's the only allowed value) |
 * | `array`               | add/remove list of recursive PropInputs        |
 * | `object`              | bordered group of recursive PropInputs         |
 * | `union` (discriminated) | variant picker + recursive object form       |
 * | `union` (other)       | JSON textarea + hint                           |
 * | `reference`           | JSON textarea + "unresolved" hint              |
 * | `unknown`             | JSON textarea + warning                        |
 *
 * The component is intentionally small and self-contained — no calls
 * into cms-core; the parent {@link ComponentBlockField} runs validation
 * via `validateComponentBlockValue` and pipes per-prop errors back in
 * via the `errors` prop.
 */

import {
  detectDiscriminator,
  findVariant,
} from '@actuate-media/component-blocks/discriminated-union'
import type { DiscriminatedUnion } from '@actuate-media/component-blocks/discriminated-union'
import type { PropSpec, PropType } from '@actuate-media/component-blocks'

import {
  defaultForType,
  parseEnumSelection,
  safeJsonStringify,
  switchUnionVariant,
} from './component-block-helpers.js'

export { defaultForType }

export interface PropInputProps {
  prop: PropSpec
  value: unknown
  onChange: (value: unknown) => void
  /** Map keyed by `propPath` (dot-joined) for nested error display. */
  errors?: Record<string, string>
  /** Dot-separated path for the field; used as the error map key. */
  path?: string
}

export function PropInput({ prop, value, onChange, errors, path }: PropInputProps) {
  const fullPath = path ?? prop.name
  const safeErrors = errors ?? EMPTY_ERRORS
  const error = safeErrors[fullPath]
  return (
    <div className="space-y-1">
      <PropInputBody
        prop={prop}
        value={value}
        onChange={onChange}
        errors={safeErrors}
        path={fullPath}
      />
      {prop.description && !error ? (
        <p className="text-xs text-[var(--muted-foreground)]">{prop.description}</p>
      ) : null}
      {error ? <p className="text-xs text-[var(--destructive)]">{error}</p> : null}
    </div>
  )
}

const EMPTY_ERRORS: Record<string, string> = Object.freeze({}) as Record<string, string>

interface PropInputBodyProps {
  prop: PropSpec
  value: unknown
  onChange: (value: unknown) => void
  errors: Record<string, string>
  path: string
}

function PropInputBody({ prop, value, onChange, errors, path }: PropInputBodyProps) {
  const type = prop.type
  switch (type.kind) {
    case 'string':
      return <StringInput label={fieldLabel(prop)} value={value} onChange={onChange} />
    case 'number':
      return <NumberInputControl label={fieldLabel(prop)} value={value} onChange={onChange} />
    case 'boolean':
      return <BooleanInput label={fieldLabel(prop)} value={value} onChange={onChange} />
    case 'enum':
      return (
        <EnumInput
          label={fieldLabel(prop)}
          options={type.values}
          value={value}
          onChange={onChange}
        />
      )
    case 'literal':
      return <LiteralDisplay label={fieldLabel(prop)} literal={type.value} />
    case 'array':
      return (
        <ArrayInput
          label={fieldLabel(prop)}
          itemType={type.itemType}
          itemPath={path}
          value={value}
          onChange={onChange}
          errors={errors}
        />
      )
    case 'object':
      return (
        <ObjectInput
          label={fieldLabel(prop)}
          fields={type.fields}
          value={value}
          onChange={onChange}
          errors={errors}
          path={path}
        />
      )
    case 'union': {
      const detected = detectDiscriminator(type)
      if (detected) {
        return (
          <DiscriminatedUnionInput
            label={fieldLabel(prop)}
            union={detected}
            value={value}
            onChange={onChange}
            errors={errors}
            path={path}
          />
        )
      }
      return (
        <JsonFallback
          label={fieldLabel(prop)}
          value={value}
          onChange={onChange}
          hint={fallbackHint(type)}
        />
      )
    }
    case 'reference':
    case 'unknown':
    default:
      return (
        <JsonFallback
          label={fieldLabel(prop)}
          value={value}
          onChange={onChange}
          hint={fallbackHint(type)}
        />
      )
  }
}

function fieldLabel(prop: PropSpec): React.ReactNode {
  return (
    <>
      <span className="font-medium">{prop.name}</span>
      {prop.required ? <span className="ml-0.5 text-[var(--destructive)]">*</span> : null}
    </>
  )
}

function fallbackHint(type: PropType): string {
  if (type.kind === 'reference') {
    return `Type '${type.targetType}' could not be resolved — paste a raw JSON value for now.`
  }
  if (type.kind === 'union') {
    return 'Non-discriminated union — paste a raw JSON value (the form renders a picker for unions with a shared literal tag field like `kind`).'
  }
  return 'Unknown type — paste a raw JSON value.'
}

// ─── Primitive editors ───────────────────────────────────────────────

function StringInput({
  label,
  value,
  onChange,
}: {
  label: React.ReactNode
  value: unknown
  onChange: (v: unknown) => void
}) {
  const stringVal = typeof value === 'string' ? value : ''
  return (
    <label className="block text-sm">
      <div className="mb-1">{label}</div>
      <input
        type="text"
        value={stringVal}
        onChange={(e) => onChange(e.target.value)}
        className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
      />
    </label>
  )
}

function NumberInputControl({
  label,
  value,
  onChange,
}: {
  label: React.ReactNode
  value: unknown
  onChange: (v: unknown) => void
}) {
  const numericVal = typeof value === 'number' ? value : ''
  return (
    <label className="block text-sm">
      <div className="mb-1">{label}</div>
      <input
        type="number"
        value={numericVal === '' ? '' : numericVal}
        onChange={(e) => {
          const raw = e.target.value
          if (raw === '') {
            onChange(undefined)
            return
          }
          const parsed = Number(raw)
          onChange(Number.isNaN(parsed) ? raw : parsed)
        }}
        className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
      />
    </label>
  )
}

function BooleanInput({
  label,
  value,
  onChange,
}: {
  label: React.ReactNode
  value: unknown
  onChange: (v: unknown) => void
}) {
  const boolVal = value === true
  return (
    <div className="flex items-center justify-between text-sm">
      <span>{label}</span>
      <button
        type="button"
        role="switch"
        aria-checked={boolVal}
        onClick={() => onChange(!boolVal)}
        className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${
          boolVal ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'
        }`}
      >
        <span
          className={`bg-card pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm ring-0 transition-transform ${
            boolVal ? 'translate-x-5' : 'translate-x-0'
          }`}
        />
      </button>
    </div>
  )
}

function EnumInput({
  label,
  options,
  value,
  onChange,
}: {
  label: React.ReactNode
  options: (string | number)[]
  value: unknown
  onChange: (v: unknown) => void
}) {
  const currentIdx = options.findIndex((o) => o === value)
  return (
    <label className="block text-sm">
      <div className="mb-1">{label}</div>
      <select
        value={currentIdx === -1 ? '' : String(currentIdx)}
        onChange={(e) => {
          onChange(parseEnumSelection(e.target.value, options))
        }}
        className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
      >
        <option value="">Select…</option>
        {options.map((opt, idx) => (
          <option key={String(opt)} value={idx}>
            {String(opt)}
          </option>
        ))}
      </select>
    </label>
  )
}

function LiteralDisplay({
  label,
  literal,
}: {
  label: React.ReactNode
  literal: string | number | boolean
}) {
  return (
    <div className="text-sm">
      <div className="mb-1">{label}</div>
      <div className="rounded-md border border-dashed border-[var(--border)] bg-[var(--muted)] px-3 py-2 text-xs text-[var(--muted-foreground)]">
        Fixed: <code>{String(literal)}</code>
      </div>
    </div>
  )
}

// ─── Composite editors ───────────────────────────────────────────────

function ObjectInput({
  label,
  fields,
  value,
  onChange,
  errors,
  path,
}: {
  label: React.ReactNode
  fields: PropSpec[]
  value: unknown
  onChange: (v: unknown) => void
  errors: Record<string, string>
  path: string
}) {
  const obj = isPlainObject(value) ? value : {}
  return (
    <fieldset className="rounded-md border border-[var(--border)] bg-[var(--card)] p-3 text-sm">
      <legend className="px-1 text-xs tracking-wide text-[var(--muted-foreground)] uppercase">
        {label}
      </legend>
      <div className="space-y-3">
        {fields.map((field) => (
          <PropInput
            key={field.name}
            prop={field}
            value={obj[field.name]}
            onChange={(next) => onChange({ ...obj, [field.name]: next })}
            errors={errors}
            path={`${path}.${field.name}`}
          />
        ))}
      </div>
    </fieldset>
  )
}

function ArrayInput({
  label,
  itemType,
  itemPath,
  value,
  onChange,
  errors,
}: {
  label: React.ReactNode
  itemType: PropType
  itemPath: string
  value: unknown
  onChange: (v: unknown) => void
  errors: Record<string, string>
}) {
  const arr = Array.isArray(value) ? value : []
  return (
    <div className="text-sm">
      <div className="mb-2 flex items-center justify-between">
        <span>{label}</span>
        <button
          type="button"
          onClick={() => onChange([...arr, defaultForType(itemType)])}
          className="rounded-md border border-[var(--border)] bg-[var(--input-background)] px-2 py-1 text-xs text-[var(--foreground)] hover:bg-[var(--accent)]"
        >
          + Add
        </button>
      </div>
      {arr.length === 0 ? (
        <div className="rounded-md border border-dashed border-[var(--border)] px-3 py-4 text-center text-xs text-[var(--muted-foreground)]">
          No items yet
        </div>
      ) : (
        <ol className="space-y-2">
          {arr.map((item, idx) => (
            <li key={idx} className="rounded-md border border-[var(--border)] bg-[var(--card)] p-3">
              <div className="mb-2 flex items-center justify-between text-xs text-[var(--muted-foreground)]">
                <span>Item {idx + 1}</span>
                <button
                  type="button"
                  onClick={() => onChange(arr.filter((_, i) => i !== idx))}
                  className="text-[var(--destructive)] hover:underline"
                >
                  Remove
                </button>
              </div>
              <PropInput
                prop={{ name: `[${idx}]`, type: itemType, required: true }}
                value={item}
                onChange={(next) => onChange(arr.map((v, i) => (i === idx ? next : v)))}
                errors={errors}
                path={`${itemPath}[${idx}]`}
              />
            </li>
          ))}
        </ol>
      )}
    </div>
  )
}

function DiscriminatedUnionInput({
  label,
  union,
  value,
  onChange,
  errors,
  path,
}: {
  label: React.ReactNode
  union: DiscriminatedUnion
  value: unknown
  onChange: (v: unknown) => void
  errors: Record<string, string>
  path: string
}) {
  const currentVariant = findVariant(value, union)
  const obj = isPlainObject(value) ? value : {}

  function switchTo(variantIdx: number) {
    const variant = union.variants[variantIdx]
    if (!variant) return
    onChange(switchUnionVariant(value, union, variant))
  }

  const currentIdx = currentVariant
    ? union.variants.findIndex((v) => v.value === currentVariant.value)
    : -1

  return (
    <fieldset className="rounded-md border border-[var(--border)] bg-[var(--card)] p-3 text-sm">
      <legend className="px-1 text-xs tracking-wide text-[var(--muted-foreground)] uppercase">
        {label}
      </legend>
      <div className="space-y-3">
        <label className="block text-sm">
          <div className="mb-1">
            <span className="font-medium">{union.field}</span>
            <span className="ml-0.5 text-[var(--destructive)]">*</span>
            <span className="ml-2 text-xs text-[var(--muted-foreground)]">(variant)</span>
          </div>
          <select
            value={currentIdx === -1 ? '' : String(currentIdx)}
            onChange={(e) => {
              const idx = Number(e.target.value)
              if (Number.isInteger(idx) && idx >= 0 && idx < union.variants.length) {
                switchTo(idx)
              }
            }}
            className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
          >
            <option value="" disabled>
              Select a variant…
            </option>
            {union.variants.map((variant, idx) => (
              <option key={String(variant.value)} value={idx}>
                {String(variant.value)}
              </option>
            ))}
          </select>
        </label>
        {currentVariant && currentVariant.remainingFields.length > 0 ? (
          <div className="space-y-3">
            {currentVariant.remainingFields.map((field) => (
              <PropInput
                key={field.name}
                prop={field}
                value={obj[field.name]}
                onChange={(next) => onChange({ ...obj, [field.name]: next })}
                errors={errors}
                path={`${path}.${field.name}`}
              />
            ))}
          </div>
        ) : null}
        {!currentVariant ? (
          <p className="text-xs text-[var(--muted-foreground)]">
            Pick a variant to configure its fields.
          </p>
        ) : null}
      </div>
    </fieldset>
  )
}

function JsonFallback({
  label,
  value,
  onChange,
  hint,
}: {
  label: React.ReactNode
  value: unknown
  onChange: (v: unknown) => void
  hint: string
}) {
  // Treat both `undefined` and `null` as "empty" — `null` arrives as
  // the structural default for kinds we can't seed (union / reference /
  // unknown) and also from a JSON round-trip on `undefined`. Rendering
  // the literal string "null" would surface as broken UX.
  const text = value === undefined || value === null ? '' : safeJsonStringify(value)
  return (
    <label className="block text-sm">
      <div className="mb-1">{label}</div>
      <textarea
        value={text}
        onChange={(e) => {
          const raw = e.target.value
          if (raw.trim() === '') {
            onChange(undefined)
            return
          }
          try {
            onChange(JSON.parse(raw))
          } catch {
            // Keep the raw string so the user can finish editing without
            // losing focus; the validator will catch it on save.
            onChange(raw)
          }
        }}
        rows={3}
        className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 font-mono text-xs outline-none focus:ring-2 focus:ring-[var(--ring)]"
      />
      <p className="mt-1 text-xs text-[var(--muted-foreground)]">{hint}</p>
    </label>
  )
}

// ─── Helpers ─────────────────────────────────────────────────────────

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return v !== null && typeof v === 'object' && !Array.isArray(v)
}
