import type { AsukaElement, ElementVariation } from '../types/elements';
import type { AsukaRenderEngineKind } from './renderEngine';
import {
  collectElementVariationLeafPaths,
  findElementForVariationInput,
  getElementVariationSyncKey,
  getProduct3DPartIdFromVariationPath,
  type AsukaElementVariationApplyInput,
} from './elementVariationSync';

export interface AsukaVariationQuery {
  elementId?: string;
  familyId?: string;
  name?: string;
  partId?: string;
}

export interface AsukaVariationDescriptor {
  id: string;
  name: string;
  elementId: string;
  familyId?: string;
  partId?: string;
  syncKey?: string;
  pathIds: string[];
  pathNames: string[];
  active: boolean;
  price?: number | string;
}

export interface AsukaVariationApplyInput extends Omit<AsukaElementVariationApplyInput, 'scope'> {
  scope?: AsukaElementVariationApplyInput['scope'];
}

export interface AsukaVariationApplyResult {
  applied: boolean;
  scope: AsukaElementVariationApplyInput['scope'];
  elementId?: string;
  familyId?: string;
  partId?: string;
  variationId: string;
  variationPathIds: string[];
  variationPathNames: string[];
  activeViewId: string | null;
  engine: AsukaRenderEngineKind;
  impactedViewIds: string[];
}

export interface AsukaVariationEvent {
  type: 'applied' | 'error';
  input: Partial<AsukaElementVariationApplyInput>;
  error?: string;
}

export type AsukaVariationListener = (event: AsukaVariationEvent) => void;

export interface AsukaVariationsApi {
  /** List leaf variations for an element, optionally restricted to one 3D part. */
  list: (query: AsukaVariationQuery) => AsukaVariationDescriptor[];
  /** Return the active leaf variation for an element or part. */
  getActive: (query: AsukaVariationQuery) => AsukaVariationDescriptor | null;
  /** Apply and synchronize one variation across linked Fabric and Babylon views. */
  apply: (input: AsukaVariationApplyInput) => Promise<AsukaVariationApplyResult>;
  /** Subscribe to successful or failed variation mutations. */
  subscribe: (listener: AsukaVariationListener) => () => void;
}

const clean = (value: unknown): string => String(value ?? '').trim();
const sameId = (left: unknown, right: unknown): boolean => {
  const a = clean(left).toLowerCase();
  const b = clean(right).toLowerCase();
  return Boolean(a && b && a === b);
};

const getNodeName = (node: any): string => clean(node?.name ?? node?.label ?? node?.id);

const makeDescriptor = (element: AsukaElement, path: ElementVariation[]): AsukaVariationDescriptor => {
  const variation = path[path.length - 1] as any;
  const partId = getProduct3DPartIdFromVariationPath(path);
  const syncKey = getElementVariationSyncKey(variation);
  const price = variation?.price;
  return {
    id: clean(variation?.id),
    name: getNodeName(variation),
    elementId: clean((element as any)?.id ?? (element as any)?.name),
    ...(clean((element as any)?.familyId) ? { familyId: clean((element as any)?.familyId) } : null),
    ...(partId ? { partId } : null),
    ...(syncKey ? { syncKey } : null),
    pathIds: path.map((node: any) => clean(node?.id)).filter(Boolean),
    pathNames: path.map(getNodeName).filter(Boolean),
    active: Boolean(variation?.active),
    ...(typeof price === 'number' || typeof price === 'string' ? { price } : null),
  };
};

export function listElementVariationDescriptors(
  catalogInput: AsukaElement[],
  query: AsukaVariationQuery,
): AsukaVariationDescriptor[] {
  const catalog = Array.isArray(catalogInput) ? catalogInput : [];
  const element = findElementForVariationInput(catalog, query);
  if (!element) return [];
  const wantedPartId = clean(query.partId);
  return collectElementVariationLeafPaths(Array.isArray(element.variations) ? element.variations : [])
    .filter((path) => !wantedPartId || sameId(getProduct3DPartIdFromVariationPath(path), wantedPartId))
    .map((path) => makeDescriptor(element, path))
    .filter((variation) => Boolean(variation.id));
}

export function normalizeVariationApplyInput(
  catalogInput: AsukaElement[],
  input: AsukaVariationApplyInput,
): AsukaElementVariationApplyInput {
  const catalog = Array.isArray(catalogInput) ? catalogInput : [];
  const base: AsukaElementVariationApplyInput = {
    ...input,
    scope: input.scope ?? 'document',
    variationId: clean(input.variationId),
  };
  const element = findElementForVariationInput(catalog, base);
  if (!element) return base;

  const wantedPartId = clean(input.partId);
  const wantedVariationId = clean(input.variationId);
  const paths = collectElementVariationLeafPaths(Array.isArray(element.variations) ? element.variations : []);
  const exactPath = paths.find((path) => {
    const leaf = path[path.length - 1];
    if (!sameId((leaf as any)?.id, wantedVariationId)) return false;
    return !wantedPartId || sameId(getProduct3DPartIdFromVariationPath(path), wantedPartId);
  }) ?? paths.find((path) => sameId((path[path.length - 1] as any)?.id, wantedVariationId));
  if (!exactPath) return {
    ...base,
    elementId: clean((element as any)?.id) || base.elementId,
    familyId: clean((element as any)?.familyId) || base.familyId,
    name: clean((element as any)?.name) || base.name,
  };

  const leaf = exactPath[exactPath.length - 1] as any;
  const partId = getProduct3DPartIdFromVariationPath(exactPath) || wantedPartId;
  return {
    ...base,
    elementId: clean((element as any)?.id) || base.elementId,
    familyId: clean((element as any)?.familyId) || base.familyId,
    name: clean((element as any)?.name) || base.name,
    variationId: clean(leaf?.id) || base.variationId,
    variationPathIds: exactPath.map((node: any) => clean(node?.id)).filter(Boolean),
    variationPathNames: exactPath.map(getNodeName).filter(Boolean),
    variationSyncKey: getElementVariationSyncKey(leaf) || base.variationSyncKey,
    partId: partId || undefined,
  };
}
