'use client';
import { FunctionComponent, useMemo, useState } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
import { useSnackbar } from 'notistack';
import { Box, Chip, MenuItem, Skeleton, Stack, TextField, Typography } from '@mui/material';
import {
  AccentCard,
  AppButton,
  AppIcon,
  AppLoading,
  CategoryTile,
  ErrorState,
  FormSection,
  PageHeader,
  RhfTextField,
  StepperHeader,
  SurfaceCard,
  VariantCard,
} from '@/components';
import { CONTENT_MAX_WIDTH } from '@/components/config';
import { ApiError } from '@/lib/api/errors';
import { digitsOnly, rialToToman, tomanToRial } from '@/utils';
import {
  useCategoryOptionGroups,
  useCreateVariant,
  useMyVariants,
  useServiceCategories,
  useUpdateVariant,
} from '@/services/catalog';
import { pickCatalogName } from '@/services/catalog/names';
import {
  PRICE_UNITS,
  optionSetSignature,
  type NurseServiceVariant,
  type PriceUnit,
  type VariantOptionSelection,
} from '@/services/catalog/types';

interface VariantBuilderProps {
  /** `null` = create (stepped flow); a variant = edit (category/options locked, price form only). */
  initial: NurseServiceVariant | null;
  onDone: () => void;
  onCancel: () => void;
  /** Jump straight into editing an already-existing listing — the create step's 409-duplicate recovery. */
  onEditExisting: (variant: NurseServiceVariant) => void;
}

const DEFAULT_UNIT: PriceUnit = 'per_hour';
const MAX_PRICE_DIGITS = 12;
const MAX_DURATION_DIGITS = 4;

type StepKey = 'category' | 'options' | 'price';

interface VariantFormValues {
  categoryId: number | null;
  /** Option-group id → chosen value id. One field, so a category switch clears the whole answer set. */
  options: Record<number, number>;
  priceToman: string;
  priceUnit: PriceUnit;
  duration: string;
  /** `null` until the nurse types over the auto-generated name — blank means "let the server name it". */
  displayNameOverride: string | null;
}

/**
 * The nurse variant builder (`CreateVariant` / `UpdateVariant`).
 *
 * **Create** walks category → options → price. Two things make the flow deterministic where it
 * previously was not:
 *
 *  1. **A step that has nothing to ask is not shown.** Categories with no option groups used to get a
 *     middle step whose entire content was "این دسته گزینه‌ای برای تنظیم ندارد" plus a Next button.
 *     The step list is now derived from the loaded groups, so those categories go straight to pricing.
 *  2. **Advancing is gated *before* the tap, not after it.** The old Next button was always enabled and
 *     surfaced an error only once pressed, from a separate `optionsError` flag. Now the unanswered
 *     required groups are named under the button while it is disabled, so the blocker is visible
 *     without probing for it.
 *
 * The final step is a review as well as a form: the chosen category and options are recapped as chips
 * beside the live `VariantCard`, so the listing can be checked without stepping backwards.
 *
 * Money still crosses the field boundary exactly once — the price is entered in **Toman** and
 * converted to an IRR digit-string via `tomanToRial` (integer-safe, never a float); the estimated
 * total is shown only from `price` × `sessionCount`, never `price` alone. A duplicate identical
 * listing (`409`) shows a friendly inline warning that offers to edit the colliding listing instead.
 *
 * **Edit** locks the category + option-set (changing them would change identity) and edits only
 * price/unit/duration/display via `update`.
 */
const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDone, onCancel, onEditExisting }) => {
  const t = useTranslations('services');
  const tCatalog = useTranslations('catalog');
  const tc = useTranslations('common');
  const locale = useLocale();
  const { enqueueSnackbar } = useSnackbar();

  const isEdit = initial !== null;

  const createVariant = useCreateVariant();
  const updateVariant = useUpdateVariant();
  const submitting = createVariant.isPending || updateVariant.isPending;

  const [step, setStep] = useState<StepKey>(isEdit ? 'price' : 'category');
  const [duplicate, setDuplicate] = useState(false);

  const form = useForm<VariantFormValues>({
    mode: 'onTouched',
    defaultValues: {
      categoryId: initial?.serviceCategoryId ?? null,
      options: {},
      // Pre-fill the price field in Toman (the wire carries IRR Rials); the money util does the ÷10.
      priceToman: initial ? String(rialToToman(initial.price)) : '',
      priceUnit: initial?.priceUnit ?? DEFAULT_UNIT,
      duration: initial?.sessionCount ? String(initial.sessionCount) : '',
      displayNameOverride: null,
    },
  });
  const { control, handleSubmit, setValue } = form;
  const categoryId = useWatch({ control, name: 'categoryId' });
  const selectedOptions = useWatch({ control, name: 'options' });
  const priceToman = useWatch({ control, name: 'priceToman' });
  const priceUnit = useWatch({ control, name: 'priceUnit' });
  const duration = useWatch({ control, name: 'duration' });
  const displayNameOverride = useWatch({ control, name: 'displayNameOverride' });

  const categoriesQuery = useServiceCategories();
  const categories = categoriesQuery.data?.items ?? [];
  const optionGroupsQuery = useCategoryOptionGroups(isEdit ? null : categoryId);
  // Stable reference so the auto-name useMemo below isn't invalidated on every render by a fresh `[]`.
  const groups = useMemo(() => optionGroupsQuery.data ?? [], [optionGroupsQuery.data]);

  const selectedCategory = categories.find((category) => category.id === categoryId) ?? null;
  const missingRequiredGroups = groups.filter((group) => group.isRequired && selectedOptions[group.id] == null);

  // A category with no option groups has no question to ask, so its step doesn't exist. Until the
  // groups for the chosen category have actually loaded the answer is unknown, and the assumption is
  // "there is an options step" — that way the step count only ever collapses for a category proven to
  // have none, instead of starting at two and growing the moment a category is tapped.
  const optionsStepUnknown = categoryId == null || optionGroupsQuery.isLoading || optionGroupsQuery.isFetching;
  const hasOptionsStep = !isEdit && (optionsStepUnknown || groups.length > 0);
  const effectiveStep: StepKey = step === 'options' && !hasOptionsStep ? 'price' : step;

  const visibleSteps: StepKey[] = hasOptionsStep ? ['category', 'options', 'price'] : ['category', 'price'];
  const stepLabels: Record<StepKey, string> = {
    category: t('step_category'),
    options: t('step_options'),
    price: t('step_price'),
  };

  // Reuses the already-cached offerings list (MyServicesList holds the same query) to resolve which
  // existing listing a 409 duplicate collided with, so the recovery can offer "edit that one" directly.
  const myVariantsQuery = useMyVariants();
  const existingMatch = useMemo(() => {
    if (isEdit || categoryId == null) return null;
    const signature = optionSetSignature(categoryId, Object.values(selectedOptions));
    return (
      (myVariantsQuery.data?.items ?? []).find(
        (variant) =>
          variant.serviceCategoryId === categoryId &&
          optionSetSignature(categoryId, variant.options.map((option) => option.optionValueId)) === signature,
      ) ?? null
    );
  }, [isEdit, categoryId, selectedOptions, myVariantsQuery.data]);

  // Auto-generated display name preview (create): category + chosen value labels, in the active locale.
  const autoName = useMemo(() => {
    if (isEdit) return initial.displayName;
    if (!selectedCategory) return '';
    const valueLabels = groups
      .map((group) => {
        const valueId = selectedOptions[group.id];
        const value = valueId == null ? null : group.values.find((candidate) => candidate.id === valueId);
        return value ? pickCatalogName(value, locale) : null;
      })
      .filter((label): label is string => label !== null);
    return [pickCatalogName(selectedCategory, locale), ...valueLabels].join(' · ');
  }, [isEdit, initial, selectedCategory, groups, selectedOptions, locale]);

  const displayValue = displayNameOverride ?? autoName;

  const priceValid = priceToman.length > 0 && BigInt(priceToman) > BigInt(0);
  const irr = priceValid ? tomanToRial(priceToman) : null;
  const durationInt = duration ? Number(duration) : 0;
  const sessionCount = durationInt > 0 ? durationInt : null;

  const selectCategory = (id: number) => {
    if (id === categoryId) return;
    // Switching category invalidates the previous category's option answers + auto-name.
    setValue('categoryId', id, { shouldDirty: true });
    setValue('options', {});
    setValue('displayNameOverride', null);
  };

  const changeOption = (groupId: number, valueId: number | null) => {
    const next = { ...selectedOptions };
    if (valueId == null) delete next[groupId];
    else next[groupId] = valueId;
    // A manual displayName override is intentionally left untouched; the auto-name preview tracks
    // option changes only while the field hasn't been overridden (displayValue = override ?? autoName).
    setValue('options', next, { shouldDirty: true });
  };

  const goNext = () => {
    if (effectiveStep === 'category') {
      setStep(hasOptionsStep ? 'options' : 'price');
      return;
    }
    setStep('price');
  };

  const goBack = () => {
    if (effectiveStep === 'price' && !isEdit) {
      setStep(hasOptionsStep ? 'options' : 'category');
      return;
    }
    setStep('category');
  };

  const submit = (values: VariantFormValues) => {
    // `handleSubmit` has already enforced the price rule; this is the type narrowing that lets the
    // IRR string be passed on, not a second gate.
    if (irr == null) return;
    const displayName = values.displayNameOverride?.trim() ? values.displayNameOverride.trim() : undefined;

    if (isEdit) {
      updateVariant.mutate(
        { id: initial.id, input: { price: irr, priceUnit: values.priceUnit, sessionCount, displayName } },
        {
          onSuccess: () => {
            enqueueSnackbar(t('saved_toast'), { variant: 'success' });
            onDone();
          },
          onError: () => enqueueSnackbar(t('create_error'), { variant: 'error' }),
        },
      );
      return;
    }

    const options: VariantOptionSelection[] = Object.entries(values.options).map(([groupId, valueId]) => ({
      optionGroupId: Number(groupId),
      optionValueId: valueId,
    }));
    createVariant.mutate(
      {
        serviceCategoryId: values.categoryId as number,
        options,
        price: irr,
        priceUnit: values.priceUnit,
        sessionCount,
        displayName,
      },
      {
        onSuccess: () => {
          enqueueSnackbar(t('created_toast'), { variant: 'success' });
          onDone();
        },
        onError: (error) => {
          // The duplicate-listing conflict is a friendly inline warning, never a generic toast.
          if (error instanceof ApiError && error.status === 409) setDuplicate(true);
          else enqueueSnackbar(t('create_error'), { variant: 'error' });
        },
      },
    );
  };

  // The price step's live preview — the actual VariantCard, so the nurse sees the listing they're
  // composing, not just an abstract price readout.
  const previewVariant: NurseServiceVariant = {
    id: 0,
    serviceCategoryId: categoryId ?? 0,
    categoryNameFa: selectedCategory?.nameFa ?? initial?.categoryNameFa ?? '',
    categoryNameEn: selectedCategory?.nameEn ?? initial?.categoryNameEn ?? '',
    price: irr ?? '0',
    priceUnit,
    sessionCount,
    displayName: displayValue || t('preview_untitled'),
    isActive: true,
    options: [],
  };

  /** Category + chosen options, so the last step doubles as a review of the first two. */
  const recapChips = isEdit
    ? initial.options.map((option) => ({
        key: String(option.optionGroupId),
        label: `${pickCatalogName({ nameFa: option.groupNameFa, nameEn: option.groupNameEn }, locale)}: ${pickCatalogName({ nameFa: option.valueNameFa, nameEn: option.valueNameEn }, locale)}`,
      }))
    : groups.flatMap((group) => {
        const value = group.values.find((candidate) => candidate.id === selectedOptions[group.id]);
        return value
          ? [{ key: String(group.id), label: `${pickCatalogName(group, locale)}: ${pickCatalogName(value, locale)}` }]
          : [];
      });

  const priceStep = (
    <Stack sx={{ gap: 2.5 }}>
      <FormSection title={t('section_recap_title')} description={t('section_recap_description')} icon="category">
        <Stack sx={{ gap: 1 }}>
          <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
            {selectedCategory
              ? pickCatalogName(selectedCategory, locale)
              : initial
                ? pickCatalogName({ nameFa: initial.categoryNameFa, nameEn: initial.categoryNameEn }, locale)
                : ''}
          </Typography>
          {isEdit ? (
            <Typography variant="caption" sx={{ color: 'text.secondary' }}>
              {t('category_locked')}
            </Typography>
          ) : null}
          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75 }}>
            {recapChips.length > 0 ? (
              recapChips.map((chip) => (
                <Chip key={chip.key} size="small" label={chip.label} sx={{ bgcolor: 'var(--bal-primary-soft)' }} />
              ))
            ) : (
              <Typography variant="caption" sx={{ color: 'text.secondary' }}>
                {t('summary_none')}
              </Typography>
            )}
          </Box>
        </Stack>
      </FormSection>

      <FormSection title={t('section_price_title')} description={t('price_hint')} icon="earnings">
        <RhfTextField<VariantFormValues>
          name="priceToman"
          label={t('price_label')}
          transform={(raw) => digitsOnly(raw).slice(0, MAX_PRICE_DIGITS)}
          rules={{
            validate: (value) => {
              const raw = String(value ?? '');
              return (raw.length > 0 && BigInt(raw) > BigInt(0)) || t('price_required');
            },
          }}
          slotProps={{ htmlInput: { dir: 'ltr', inputMode: 'numeric', style: { textAlign: 'start' } } }}
          fullWidth
        />

        <Stack direction={{ xs: 'column', sm: 'row' }} sx={{ gap: 2 }}>
          <RhfTextField<VariantFormValues> name="priceUnit" select label={t('unit_label')} fullWidth>
            {PRICE_UNITS.map((unit) => (
              <MenuItem key={unit} value={unit}>
                {tCatalog(`unit_${unit}`)}
              </MenuItem>
            ))}
          </RhfTextField>

          <RhfTextField<VariantFormValues>
            name="duration"
            label={t('duration_label')}
            helperText={t('duration_hint')}
            transform={(raw) => digitsOnly(raw).slice(0, MAX_DURATION_DIGITS)}
            slotProps={{ htmlInput: { dir: 'ltr', inputMode: 'numeric', style: { textAlign: 'start' } } }}
            fullWidth
          />
        </Stack>

        {!sessionCount && irr ? (
          <Typography variant="caption" sx={{ color: 'text.secondary' }}>
            {t('rate_note')}
          </Typography>
        ) : null}
      </FormSection>

      <FormSection title={t('section_listing_title')} description={t('display_name_hint')} icon="services">
        {/* Not `RhfTextField`: what is *stored* is the override alone (blank ⇒ the server generates
            the name), while what is *shown* falls back to the live auto-generated name. One field,
            two values — the one case in this form where the display value isn't the form value. */}
        <Controller
          control={control}
          name="displayNameOverride"
          render={({ field }) => (
            <TextField
              label={t('display_name_label')}
              name={field.name}
              inputRef={field.ref}
              value={field.value ?? autoName}
              onChange={(event) => field.onChange(event.target.value)}
              onBlur={field.onBlur}
              fullWidth
            />
          )}
        />

        <Stack sx={{ gap: 1 }}>
          <Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 700 }}>
            {t('preview_heading')}
          </Typography>
          {/* Shown even before a price is entered — the preview is what "comprehensive" means here:
              the nurse should see the shape of the listing while composing it, not only once it's valid. */}
          <VariantCard variant={previewVariant} interactive={false} />
        </Stack>
      </FormSection>

      {duplicate ? (
        <AccentCard tone="warning" padding="sm">
          <Stack sx={{ gap: 1 }}>
            <Stack direction="row" sx={{ gap: 1, alignItems: 'flex-start' }}>
              <AppIcon icon="warning" size={20} color="var(--bal-warning)" />
              {/* Warning is reserved for the edge + icon; the body reads as normal text, not amber-on-paper. */}
              <Typography variant="body2" sx={{ color: 'text.primary', fontWeight: 500 }}>
                {t('duplicate_warning')}
              </Typography>
            </Stack>
            {existingMatch ? (
              <AppButton
                variant="text"
                color="primary"
                startIcon="edit"
                onClick={() => onEditExisting(existingMatch)}
                sx={{ alignSelf: 'flex-start' }}
              >
                {t('duplicate_edit_existing')}
              </AppButton>
            ) : null}
          </Stack>
        </AccentCard>
      ) : null}
    </Stack>
  );

  return (
    <FormProvider {...form}>
      <Box
        component="form"
        noValidate
        onSubmit={handleSubmit(submit)}
        sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxWidth: CONTENT_MAX_WIDTH, mx: 'auto', width: '100%' }}
      >
        <PageHeader title={isEdit ? t('builder_edit_title') : t('builder_add_title')} />

        {isEdit ? null : (
          <StepperHeader
            steps={visibleSteps.map((key) => stepLabels[key])}
            activeStep={visibleSteps.indexOf(effectiveStep)}
          />
        )}

        {effectiveStep === 'category' ? (
          <FormSection title={t('category_title')} description={t('category_subtitle')} icon="category">
            {categoriesQuery.isLoading ? (
              <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 1.5 }}>
                {[0, 1, 2, 3].map((key) => (
                  <Skeleton key={key} variant="rounded" height={116} sx={{ borderRadius: 'var(--bal-radius-md)' }} />
                ))}
              </Box>
            ) : categoriesQuery.isError ? (
              <ErrorState message={t('categories_error')} retryLabel={tc('retry')} onRetry={() => categoriesQuery.refetch()} />
            ) : (
              <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 1.5 }}>
                {categories.map((category) => (
                  <CategoryTile
                    key={category.id}
                    label={pickCatalogName(category, locale)}
                    iconKey={category.iconKey}
                    selected={categoryId === category.id}
                    onClick={() => selectCategory(category.id)}
                  />
                ))}
              </Box>
            )}
          </FormSection>
        ) : null}

        {effectiveStep === 'options' ? (
          <FormSection title={t('options_title')} description={t('options_subtitle')} icon="tune">
            {optionGroupsQuery.isLoading ? (
              <AppLoading />
            ) : optionGroupsQuery.isError ? (
              // A failed fetch must never read as "this category has zero options" — that would let the
              // nurse skip required options entirely. Block progression until the retry succeeds.
              <ErrorState message={t('options_error')} retryLabel={tc('retry')} onRetry={() => optionGroupsQuery.refetch()} />
            ) : (
              groups.map((group) => (
                <Stack key={group.id} sx={{ gap: 1 }}>
                  <Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
                    <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
                      {pickCatalogName(group, locale)}
                    </Typography>
                    <Chip
                      size="small"
                      label={group.isRequired ? t('required_badge') : t('optional_badge')}
                      sx={{
                        bgcolor: group.isRequired ? 'var(--bal-primary-soft)' : 'var(--bal-divider)',
                        color: group.isRequired ? 'var(--bal-primary)' : 'text.secondary',
                        fontWeight: 500,
                      }}
                    />
                  </Stack>
                  <Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
                    {group.values.map((value) => {
                      const selected = selectedOptions[group.id] === value.id;
                      return (
                        <Chip
                          key={value.id}
                          label={pickCatalogName(value, locale)}
                          aria-pressed={selected}
                          clickable
                          color={selected ? 'primary' : 'default'}
                          variant={selected ? 'filled' : 'outlined'}
                          onClick={() => changeOption(group.id, selected ? null : value.id)}
                        />
                      );
                    })}
                  </Stack>
                </Stack>
              ))
            )}
          </FormSection>
        ) : null}

        {effectiveStep === 'price' ? priceStep : null}

        {/* Names what is still missing while the button is disabled, instead of revealing it on tap. */}
        {effectiveStep === 'options' && missingRequiredGroups.length > 0 ? (
          <Typography variant="caption" sx={{ color: 'text.secondary' }}>
            {t('options_missing_named', {
              groups: missingRequiredGroups.map((group) => pickCatalogName(group, locale)).join('، '),
            })}
          </Typography>
        ) : null}

        <SurfaceCard padding="sm" sx={{ position: 'sticky', bottom: 'var(--bal-chrome-bottom, 0px)', zIndex: 1 }}>
          <Stack direction="row" sx={{ gap: 1, justifyContent: 'space-between' }}>
            <AppButton
              variant="text"
              onClick={effectiveStep === 'category' || isEdit ? onCancel : goBack}
              disabled={submitting}
            >
              {effectiveStep === 'category' || isEdit ? tc('cancel') : tc('back')}
            </AppButton>

            {effectiveStep === 'price' ? (
              <AppButton type="submit" color="primary" variant="contained" disabled={submitting}>
                {submitting ? tc('saving') : isEdit ? t('submit_save') : t('submit_create')}
              </AppButton>
            ) : (
              <AppButton
                color="primary"
                variant="contained"
                onClick={goNext}
                disabled={
                  effectiveStep === 'category'
                    ? categoryId == null
                    : optionGroupsQuery.isError || missingRequiredGroups.length > 0
                }
              >
                {t('next')}
              </AppButton>
            )}
          </Stack>
        </SurfaceCard>
      </Box>
    </FormProvider>
  );
};

export default VariantBuilder;
