import type { AsukaCameraLimits, AsukaCameraView } from '../types/camera';
import type { AsukaElement, ElementVariation } from '../types/elements';

const finite = (value: unknown, fallback: number): number => {
  const next = Number(value);
  return Number.isFinite(next) ? next : fallback;
};

const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));

/** Normalize a public camera view before it is persisted or sent to an engine. */
export function normalizeCameraView(input: Partial<AsukaCameraView> | null | undefined): AsukaCameraView | null {
  if (!input || typeof input !== 'object') return null;
  const target = Array.isArray(input.target) && input.target.length >= 3
    ? [finite(input.target[0], 0), finite(input.target[1], 0), finite(input.target[2], 0)] as [number, number, number]
    : [0, 0, 0] as [number, number, number];
  const distance = Math.max(0.01, finite(input.distance, 1));
  const view: AsukaCameraView = {
    turn: finite(input.turn, 0),
    tilt: clamp(finite(input.tilt, 0), -89.9, 89.9),
    distance,
    target,
  };
  const fov = Number(input.fov);
  if (Number.isFinite(fov)) view.fov = clamp(fov, 1, 179);
  return view;
}

/** Normalize camera limits while keeping a valid vertical range. */
export function normalizeCameraLimits(
  input: Partial<AsukaCameraLimits> | null | undefined,
  fallback: AsukaCameraLimits = { minTilt: 2, maxTilt: 85, maxTurn: 180, turnCenter: 90 },
): AsukaCameraLimits {
  const minTilt = clamp(finite(input?.minTilt, fallback.minTilt), -89.9, 89.8);
  const maxTilt = clamp(finite(input?.maxTilt, fallback.maxTilt), minTilt + 0.1, 89.9);
  return {
    minTilt,
    maxTilt,
    maxTurn: clamp(Math.abs(finite(input?.maxTurn, fallback.maxTurn)), 0, 180),
    turnCenter: finite(input?.turnCenter, fallback.turnCenter),
  };
}

/** Find one recursive variation by id. */
export function findVariationById(variations: ElementVariation[] | null | undefined, variationId: string): ElementVariation | null {
  const wanted = String(variationId ?? '').trim();
  if (!wanted) return null;
  for (const variation of Array.isArray(variations) ? variations : []) {
    if (String(variation?.id ?? '').trim() === wanted) return variation;
    const nested = findVariationById(variation?.variations, wanted);
    if (nested) return nested;
  }
  return null;
}

/** Return the closest camera preset defined on a variation path, starting at the selected leaf. */
export function getCameraFromVariationPath(path: Array<ElementVariation | Record<string, unknown>> | null | undefined): AsukaCameraView | null {
  const nodes = Array.isArray(path) ? path : [];
  for (let index = nodes.length - 1; index >= 0; index -= 1) {
    const camera = normalizeCameraView((nodes[index] as any)?.camera);
    if (camera) return camera;
  }
  return null;
}

/** Return the camera preset stored on one exact element variation. */
export function getVariationCamera(element: AsukaElement | null | undefined, variationId: string): AsukaCameraView | null {
  return normalizeCameraView((findVariationById(element?.variations, variationId) as any)?.camera);
}

/** Clone an element and set or clear one exact variation camera preset. */
export function setVariationCamera(
  element: AsukaElement,
  variationId: string,
  camera: Partial<AsukaCameraView> | null | undefined,
): AsukaElement {
  const wanted = String(variationId ?? '').trim();
  if (!wanted) return element;
  const normalized = normalizeCameraView(camera);
  let changed = false;
  const walk = (items: ElementVariation[]): ElementVariation[] => items.map((variation) => {
    const children = Array.isArray(variation?.variations) ? walk(variation.variations) : variation?.variations;
    if (String(variation?.id ?? '').trim() !== wanted) {
      return children === variation?.variations ? variation : { ...variation, variations: children };
    }
    changed = true;
    const next: ElementVariation = { ...variation, ...(children ? { variations: children } : {}) };
    if (normalized) (next as any).camera = normalized;
    else delete (next as any).camera;
    return next;
  });
  const variations = walk(Array.isArray(element?.variations) ? element.variations : []);
  return changed ? { ...element, variations } : element;
}

/** Flatten recursive variations for selectors while preserving a readable path label. */
export function listVariationCameraTargets(element: AsukaElement | null | undefined): Array<{ id: string; label: string; depth: number; camera: AsukaCameraView | null }> {
  const out: Array<{ id: string; label: string; depth: number; camera: AsukaCameraView | null }> = [];
  const walk = (items: ElementVariation[], parents: string[], depth: number) => {
    for (const variation of Array.isArray(items) ? items : []) {
      const id = String(variation?.id ?? '').trim();
      if (!id) continue;
      const name = String(variation?.name ?? id).trim() || id;
      const path = [...parents, name];
      out.push({ id, label: path.join(' / '), depth, camera: normalizeCameraView((variation as any)?.camera) });
      walk(Array.isArray(variation?.variations) ? variation.variations : [], path, depth + 1);
    }
  };
  walk(Array.isArray(element?.variations) ? element.variations : [], [], 0);
  return out;
}
