import { sleep } from '@/utils';
import { ApiError } from '@/lib/api/errors';
import type { PageParams, Paginated } from '@/lib/api/types';
import { CATEGORIES_PAGE_SIZE, MY_VARIANTS_PAGE_SIZE } from '../constants';
import {
  isGroupApplicable,
  optionSetSignature,
  type CatalogApi,
  type CreateVariantInput,
  type NurseServiceVariant,
  type ServiceCategory,
  type ServiceOptionGroup,
  type ServiceOptionValue,
  type UpdateVariantInput,
  type VariantOption,
  type VariantOptionSelection,
} from '../types';
import { SEED_CATEGORIES, SEED_OPTION_GROUPS } from './seed';

const MOCK_LATENCY_MS = 250;
const DIGITS_ONLY = /^\d+$/;

const bySortOrder = <T extends { sortOrder: number }>(a: T, b: T) => a.sortOrder - b.sortOrder;

// The nurse's own offerings, seeded **empty** so the offerings empty-state demos; the nurse builds
// variants live (across price units), and the duplicate 409 is reachable by repeating a create.
let store: NurseServiceVariant[] = [];
let nextVariantId = 1;

/** Active-first (contract list order), then most-recent within each group. */
const orderedVariants = () =>
  [...store].sort((a, b) => Number(b.isActive) - Number(a.isActive) || b.id - a.id);

function paginate<T>(all: T[], params?: PageParams, defaultSize = 50): Paginated<T> {
  const page = params?.page ?? 1;
  const pageSize = params?.pageSize ?? defaultSize;
  const start = (page - 1) * pageSize;
  return { items: all.slice(start, start + pageSize), total: all.length, page, pageSize };
}

function applicableGroups(categoryId: number): ServiceOptionGroup[] {
  return SEED_OPTION_GROUPS.filter((group) => group.isActive && isGroupApplicable(group, categoryId)).sort(
    bySortOrder,
  );
}

function findValue(group: ServiceOptionGroup, valueId: number): ServiceOptionValue | undefined {
  return group.values.find((value) => value.id === valueId && value.isActive);
}

/** Builds the denormalised `options` array (group + value labels) from the chosen value ids. */
function buildOptions(groups: ServiceOptionGroup[], selections: VariantOptionSelection[]): VariantOption[] {
  return selections
    .map((selection) => {
      const group = groups.find((candidate) => candidate.id === selection.optionGroupId);
      const value = group && findValue(group, selection.optionValueId);
      if (!group || !value) return null;
      return {
        optionGroupId: group.id,
        groupNameFa: group.nameFa,
        groupNameEn: group.nameEn,
        optionValueId: value.id,
        valueNameFa: value.nameFa,
        valueNameEn: value.nameEn,
      } satisfies VariantOption;
    })
    .filter((option): option is VariantOption => option !== null)
    // Present in the group's configured order (mirrors how the server denormalises).
    .sort((a, b) => {
      const orderOf = (groupId: number) => groups.find((g) => g.id === groupId)?.sortOrder ?? 0;
      return orderOf(a.optionGroupId) - orderOf(b.optionGroupId);
    });
}

/** Auto-generates `displayName` from the category + chosen value labels (fa primary, contract style). */
function autoDisplayName(category: ServiceCategory, options: VariantOption[]): string {
  if (options.length === 0) return category.nameFa;
  return [category.nameFa, ...options.map((option) => option.valueNameFa)].join(' · ');
}

/**
 * Validates a create request exactly as the server does and throws the matching `ApiError` so the
 * builder exercises the real failure copy: 400 for a bad price/unit/session, a missing required
 * dimension (named), a value not in its group, a group answered twice, or an unknown/inapplicable
 * group/value; 409 for a duplicate identical listing.
 */
function assertValidCreate(category: ServiceCategory, groups: ServiceOptionGroup[], input: CreateVariantInput): void {
  if (typeof input.price !== 'string' || !DIGITS_ONLY.test(input.price) || BigInt(input.price) <= BigInt(0)) {
    throw new ApiError(400, 'Invalid price', 'invalid_price');
  }
  if (input.sessionCount != null && (!Number.isInteger(input.sessionCount) || input.sessionCount <= 0)) {
    throw new ApiError(400, 'Invalid session count', 'invalid_session_count');
  }

  const answeredGroupIds = new Set<number>();
  for (const selection of input.options) {
    const group = groups.find((candidate) => candidate.id === selection.optionGroupId);
    if (!group) throw new ApiError(400, 'Unknown or inapplicable option group', 'unknown_group');
    if (answeredGroupIds.has(group.id)) throw new ApiError(400, 'A dimension was answered twice', 'duplicate_group');
    if (!findValue(group, selection.optionValueId)) {
      throw new ApiError(400, 'Value does not belong to its group', 'invalid_value');
    }
    answeredGroupIds.add(group.id);
  }

  const missingRequired = groups.find((group) => group.isRequired && !answeredGroupIds.has(group.id));
  if (missingRequired) {
    throw new ApiError(400, `Missing required dimension: ${missingRequired.nameFa}`, 'missing_required_dimension');
  }

  const signature = optionSetSignature(
    category.id,
    input.options.map((option) => option.optionValueId),
  );
  const duplicate = store.some(
    (variant) => optionSetSignature(variant.serviceCategoryId, variant.options.map((o) => o.optionValueId)) === signature,
  );
  if (duplicate) {
    throw new ApiError(409, 'A variant with these details already exists', 'duplicate_listing');
  }
}

/**
 * In-memory mock behind the CatalogApi seam. Enforces the server's required-dimension validation
 * and the `(nurse, category, option-set)` duplicate 409 in-memory, so the builder's inline
 * validation + duplicate warning are demonstrable end-to-end. Mirrors the real shapes for a one-line swap.
 */
export const catalogMockApi: CatalogApi = {
  listCategories: async (params) => {
    await sleep(MOCK_LATENCY_MS);
    const active = SEED_CATEGORIES.filter((category) => category.isActive).sort(bySortOrder);
    return paginate(active, params, CATEGORIES_PAGE_SIZE);
  },

  getCategoryOptionGroups: async (categoryId) => {
    await sleep(MOCK_LATENCY_MS);
    // Return only active values, ordered — mirror the server's projection.
    return applicableGroups(categoryId).map((group) => ({
      ...group,
      values: group.values.filter((value) => value.isActive).sort(bySortOrder),
    }));
  },

  listMyVariants: async (params) => {
    await sleep(MOCK_LATENCY_MS);
    return paginate(orderedVariants(), params, MY_VARIANTS_PAGE_SIZE);
  },

  getVariant: async (id) => {
    await sleep(MOCK_LATENCY_MS);
    const variant = store.find((candidate) => candidate.id === id);
    if (!variant) throw new ApiError(404, 'Variant not found', 'not_found');
    return variant;
  },

  createVariant: async (input) => {
    await sleep(MOCK_LATENCY_MS);
    const category = SEED_CATEGORIES.find((candidate) => candidate.isActive && candidate.id === input.serviceCategoryId);
    if (!category) throw new ApiError(400, 'Missing or inactive category', 'invalid_category');

    const groups = applicableGroups(category.id);
    assertValidCreate(category, groups, input);

    const options = buildOptions(groups, input.options);
    const displayName = input.displayName?.trim() ? input.displayName.trim() : autoDisplayName(category, options);
    const variant: NurseServiceVariant = {
      id: nextVariantId++,
      serviceCategoryId: category.id,
      categoryNameFa: category.nameFa,
      categoryNameEn: category.nameEn,
      price: input.price,
      priceUnit: input.priceUnit,
      sessionCount: input.sessionCount ?? null,
      displayName,
      isActive: true,
      options,
    };
    store = [variant, ...store];
    return variant;
  },

  updateVariant: async (id, input: UpdateVariantInput) => {
    await sleep(MOCK_LATENCY_MS);
    const existing = store.find((candidate) => candidate.id === id);
    if (!existing) throw new ApiError(404, 'Variant not found', 'not_found');
    if (!DIGITS_ONLY.test(input.price) || BigInt(input.price) <= BigInt(0)) {
      throw new ApiError(400, 'Invalid price', 'invalid_price');
    }
    // The option-set is immutable on update — only price/unit/session/display change. A blank
    // displayName leaves the current one unchanged.
    const updated: NurseServiceVariant = {
      ...existing,
      price: input.price,
      priceUnit: input.priceUnit,
      sessionCount: input.sessionCount ?? null,
      displayName: input.displayName?.trim() ? input.displayName.trim() : existing.displayName,
    };
    store = store.map((candidate) => (candidate.id === id ? updated : candidate));
    return updated;
  },

  setVariantActive: async (id, isActive) => {
    await sleep(MOCK_LATENCY_MS);
    const existing = store.find((candidate) => candidate.id === id);
    if (!existing) throw new ApiError(404, 'Variant not found', 'not_found');
    store = store.map((candidate) => (candidate.id === id ? { ...candidate, isActive } : candidate));
  },
};
