import type {
  Point,
  Wall,
  WallOpening,
  ElectricalItem,
  FurnitureItem,
  WallDirection,
  OpeningType,
  ElectricalType,
  FurnitureType,
  RoomFull,
} from '@house-plan-maker/shared';
import {
  WALL_DIRECTIONS,
  OPENING_TYPES,
  ELECTRICAL_TYPES,
  FURNITURE_TYPES,
} from '@house-plan-maker/shared';
import type { EditorState } from '../types';
import { generateLocalId } from '../utils/geometry';

// ── Export Format ──

interface RoomExportData {
  readonly version: 1;
  readonly room: {
    readonly name: string;
    readonly shape: readonly Point[];
    readonly wallHeight: number;
    readonly plinthHeight: number;
    readonly plinthThickness: number;
  };
  readonly walls: readonly {
    readonly startX: number;
    readonly startY: number;
    readonly endX: number;
    readonly endY: number;
    readonly thickness: number;
    readonly direction: string;
  }[];
  readonly openings: readonly {
    readonly wallIndex: number;
    readonly type: string;
    readonly positionAlongWall: number;
    readonly width: number;
    readonly height: number;
    readonly elevationFromFloor: number;
  }[];
  readonly electricalItems: readonly {
    readonly type: string;
    readonly x: number;
    readonly y: number;
    readonly wallIndex: number | null;
    readonly elevationFromFloor: number | null;
    readonly rotation: number;
    readonly metadata: Record<string, unknown> | null;
  }[];
  readonly furnitureItems: readonly {
    readonly type: string;
    readonly x: number;
    readonly y: number;
    readonly width: number;
    readonly depth: number;
    readonly height: number;
    readonly rotation: number;
    readonly label: string | null;
  }[];
}

// ── Import Result ──

interface ImportResult {
  readonly room: {
    readonly name: string;
    readonly shape: readonly Point[];
    readonly wallHeight: number;
    readonly plinthHeight: number;
    readonly plinthThickness: number;
  };
  readonly walls: readonly Wall[];
  readonly openings: readonly WallOpening[];
  readonly electricalItems: readonly ElectricalItem[];
  readonly furnitureItems: readonly FurnitureItem[];
}

// ── Export ──

export function exportRoomToJson(state: EditorState): string {
  const { room, walls, openings, electricalItems, furnitureItems } = state;

  // Build wall ID -> index map
  const wallIdToIndex = new Map<string, number>();
  for (let i = 0; i < walls.length; i++) {
    wallIdToIndex.set(walls[i].id, i);
  }

  const exportData: RoomExportData = {
    version: 1,
    room: {
      name: room.name,
      shape: room.shape,
      wallHeight: room.wallHeight,
      plinthHeight: room.plinthHeight,
      plinthThickness: room.plinthThickness,
    },
    walls: walls.map((w) => ({
      startX: w.startX,
      startY: w.startY,
      endX: w.endX,
      endY: w.endY,
      thickness: w.thickness,
      direction: w.direction,
    })),
    openings: openings.map((o) => ({
      wallIndex: wallIdToIndex.get(o.wallId) ?? -1,
      type: o.type,
      positionAlongWall: o.positionAlongWall,
      width: o.width,
      height: o.height,
      elevationFromFloor: o.elevationFromFloor,
    })),
    electricalItems: electricalItems.map((e) => ({
      type: e.type,
      x: e.x,
      y: e.y,
      wallIndex: e.wallId !== null ? (wallIdToIndex.get(e.wallId) ?? null) : null,
      elevationFromFloor: e.elevationFromFloor,
      rotation: e.rotation,
      metadata: e.metadata,
    })),
    furnitureItems: furnitureItems.map((f) => ({
      type: f.type,
      x: f.x,
      y: f.y,
      width: f.width,
      depth: f.depth,
      height: f.height,
      rotation: f.rotation,
      label: f.label,
    })),
  };

  return JSON.stringify(exportData, null, 2);
}

// ── Validation Helpers ──

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

function isNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isPoint(value: unknown): value is Point {
  return isRecord(value) && isNumber(value.x) && isNumber(value.y);
}

function assertField<T>(
  obj: Record<string, unknown>,
  field: string,
  check: (v: unknown) => v is T,
  label: string,
): T {
  const value = obj[field];
  if (!check(value)) {
    throw new Error(`Invalid or missing field "${field}" in ${label}`);
  }
  return value;
}

function validateWallDirection(value: string): WallDirection {
  if (!(WALL_DIRECTIONS as readonly string[]).includes(value)) {
    throw new Error(`Invalid wall direction: "${value}". Expected one of: ${WALL_DIRECTIONS.join(', ')}`);
  }
  return value as WallDirection;
}

function validateOpeningType(value: string): OpeningType {
  if (!(OPENING_TYPES as readonly string[]).includes(value)) {
    throw new Error(`Invalid opening type: "${value}". Expected one of: ${OPENING_TYPES.join(', ')}`);
  }
  return value as OpeningType;
}

function validateElectricalType(value: string): ElectricalType {
  if (!(ELECTRICAL_TYPES as readonly string[]).includes(value)) {
    throw new Error(`Invalid electrical type: "${value}". Expected one of: ${ELECTRICAL_TYPES.join(', ')}`);
  }
  return value as ElectricalType;
}

function validateFurnitureType(value: string): FurnitureType {
  if (!(FURNITURE_TYPES as readonly string[]).includes(value)) {
    throw new Error(`Invalid furniture type: "${value}". Expected one of: ${FURNITURE_TYPES.join(', ')}`);
  }
  return value as FurnitureType;
}

// ── Import ──

export function importRoomFromJson(json: string): ImportResult {
  let parsed: unknown;
  try {
    parsed = JSON.parse(json);
  } catch {
    throw new Error('Invalid JSON: the file does not contain valid JSON data.');
  }

  if (!isRecord(parsed)) {
    throw new Error('Invalid format: expected a JSON object at the top level.');
  }

  // Version check
  if (parsed.version !== 1) {
    throw new Error(
      `Unsupported format version: ${String(parsed.version)}. Expected version 1.`,
    );
  }

  // ── Room ──
  if (!isRecord(parsed.room)) {
    throw new Error('Missing or invalid "room" field.');
  }
  const roomData = parsed.room;
  const roomName = assertField(roomData, 'name', isString, 'room');
  const roomWallHeight = assertField(roomData, 'wallHeight', isNumber, 'room');
  const roomPlinthHeight = assertField(roomData, 'plinthHeight', isNumber, 'room');
  const roomPlinthThickness = assertField(roomData, 'plinthThickness', isNumber, 'room');

  if (!Array.isArray(roomData.shape)) {
    throw new Error('Missing or invalid "room.shape" field: expected an array of points.');
  }
  const shape: Point[] = [];
  for (let i = 0; i < roomData.shape.length; i++) {
    const pt = roomData.shape[i];
    if (!isPoint(pt)) {
      throw new Error(`Invalid point at room.shape[${i}]: expected { x: number, y: number }.`);
    }
    shape.push({ x: pt.x, y: pt.y });
  }

  // ── Walls ──
  if (!Array.isArray(parsed.walls)) {
    throw new Error('Missing or invalid "walls" field: expected an array.');
  }

  const roomId = generateLocalId();
  const wallIds: string[] = [];
  const walls: Wall[] = [];

  for (let i = 0; i < parsed.walls.length; i++) {
    const w = parsed.walls[i];
    if (!isRecord(w)) {
      throw new Error(`Invalid wall at index ${i}: expected an object.`);
    }
    const wallId = generateLocalId();
    wallIds.push(wallId);

    walls.push({
      id: wallId,
      roomId,
      startX: assertField(w, 'startX', isNumber, `walls[${i}]`),
      startY: assertField(w, 'startY', isNumber, `walls[${i}]`),
      endX: assertField(w, 'endX', isNumber, `walls[${i}]`),
      endY: assertField(w, 'endY', isNumber, `walls[${i}]`),
      thickness: assertField(w, 'thickness', isNumber, `walls[${i}]`),
      direction: validateWallDirection(
        assertField(w, 'direction', isString, `walls[${i}]`),
      ),
    });
  }

  // ── Openings ──
  if (!Array.isArray(parsed.openings)) {
    throw new Error('Missing or invalid "openings" field: expected an array.');
  }

  const openings: WallOpening[] = [];
  for (let i = 0; i < parsed.openings.length; i++) {
    const o = parsed.openings[i];
    if (!isRecord(o)) {
      throw new Error(`Invalid opening at index ${i}: expected an object.`);
    }
    const wallIndex = assertField(o, 'wallIndex', isNumber, `openings[${i}]`);
    if (!Number.isInteger(wallIndex) || wallIndex < 0 || wallIndex >= walls.length) {
      throw new Error(
        `Invalid wallIndex ${wallIndex} in openings[${i}]: must be 0..${walls.length - 1}.`,
      );
    }

    openings.push({
      id: generateLocalId(),
      roomId,
      wallId: wallIds[wallIndex],
      type: validateOpeningType(
        assertField(o, 'type', isString, `openings[${i}]`),
      ),
      positionAlongWall: assertField(o, 'positionAlongWall', isNumber, `openings[${i}]`),
      width: assertField(o, 'width', isNumber, `openings[${i}]`),
      height: assertField(o, 'height', isNumber, `openings[${i}]`),
      elevationFromFloor: assertField(o, 'elevationFromFloor', isNumber, `openings[${i}]`),
      openDirection: (isString((o as Record<string, unknown>).openDirection) ? (o as Record<string, unknown>).openDirection as string : 'LEFT') as WallOpening['openDirection'],
      // Imported openings use the canonical anchor (centre/bottom) — see notes
      // in WallOpening for why this is the default for openings.
      positionAnchor: { horizontal: 'middle', vertical: 'bottom' },
      gridCols: isNumber(o.gridCols) && o.gridCols >= 1 ? Math.round(o.gridCols) : 2,
      gridRows: isNumber(o.gridRows) && o.gridRows >= 1 ? Math.round(o.gridRows) : 2,
      slopeDepth: isNumber(o.slopeDepth) && o.slopeDepth >= 0 ? Math.min(2, o.slopeDepth) : 0,
      frameThickness: isNumber(o.frameThickness) && o.frameThickness >= 0
        ? Math.min(0.5, o.frameThickness)
        : 0.03,
    });
  }

  // ── Electrical Items ──
  if (!Array.isArray(parsed.electricalItems)) {
    throw new Error('Missing or invalid "electricalItems" field: expected an array.');
  }

  const electricalItems: ElectricalItem[] = [];
  for (let i = 0; i < parsed.electricalItems.length; i++) {
    const e = parsed.electricalItems[i];
    if (!isRecord(e)) {
      throw new Error(`Invalid electrical item at index ${i}: expected an object.`);
    }
    const wallIndex = e.wallIndex;
    let wallId: string | null = null;
    if (wallIndex !== null) {
      if (!isNumber(wallIndex) || !Number.isInteger(wallIndex) || wallIndex < 0 || wallIndex >= walls.length) {
        throw new Error(
          `Invalid wallIndex ${String(wallIndex)} in electricalItems[${i}]: must be null or 0..${walls.length - 1}.`,
        );
      }
      wallId = wallIds[wallIndex];
    }

    const metadata = e.metadata;
    let validatedMetadata: Record<string, unknown> | null = null;
    if (metadata !== null && metadata !== undefined) {
      if (!isRecord(metadata)) {
        throw new Error(`Invalid metadata in electricalItems[${i}]: expected an object or null.`);
      }
      validatedMetadata = metadata;
    }

    const importedCount = isNumber(e.count) && e.count >= 1 ? Math.round(e.count) : 1;
    electricalItems.push({
      id: generateLocalId(),
      roomId,
      type: validateElectricalType(
        assertField(e, 'type', isString, `electricalItems[${i}]`),
      ),
      x: assertField(e, 'x', isNumber, `electricalItems[${i}]`),
      y: assertField(e, 'y', isNumber, `electricalItems[${i}]`),
      wallId,
      elevationFromFloor: e.elevationFromFloor === null ? null : assertField(e, 'elevationFromFloor', isNumber, `electricalItems[${i}]`),
      rotation: assertField(e, 'rotation', isNumber, `electricalItems[${i}]`),
      count: importedCount,
      positionAnchor: { horizontal: 'middle', vertical: 'middle' },
      label: typeof e.label === 'string' ? e.label : null,
      metadata: validatedMetadata,
    });
  }

  // ── Furniture Items ──
  if (!Array.isArray(parsed.furnitureItems)) {
    throw new Error('Missing or invalid "furnitureItems" field: expected an array.');
  }

  const furnitureItems: FurnitureItem[] = [];
  for (let i = 0; i < parsed.furnitureItems.length; i++) {
    const f = parsed.furnitureItems[i];
    if (!isRecord(f)) {
      throw new Error(`Invalid furniture item at index ${i}: expected an object.`);
    }

    const label = f.label;
    if (label !== null && !isString(label)) {
      throw new Error(`Invalid label in furnitureItems[${i}]: expected a string or null.`);
    }

    furnitureItems.push({
      id: generateLocalId(),
      roomId,
      type: validateFurnitureType(
        assertField(f, 'type', isString, `furnitureItems[${i}]`),
      ),
      x: assertField(f, 'x', isNumber, `furnitureItems[${i}]`),
      y: assertField(f, 'y', isNumber, `furnitureItems[${i}]`),
      width: assertField(f, 'width', isNumber, `furnitureItems[${i}]`),
      depth: assertField(f, 'depth', isNumber, `furnitureItems[${i}]`),
      height: assertField(f, 'height', isNumber, `furnitureItems[${i}]`),
      rotation: assertField(f, 'rotation', isNumber, `furnitureItems[${i}]`),
      elevationFromFloor: isNumber((f as Record<string, unknown>).elevationFromFloor) ? (f as Record<string, unknown>).elevationFromFloor as number : 0,
      label: (label as string | null) ?? null,
      // Imported furniture defaults to the legacy top-left anchor so the
      // (x, y) values from the export file (which were saved as top-left)
      // continue to refer to the same physical point.
      positionAnchor: { horizontal: 'left', vertical: 'top' },
      metadata: isRecord(f.metadata) ? (f.metadata as Record<string, unknown>) : null,
    });
  }

  return {
    room: {
      name: roomName,
      shape,
      wallHeight: roomWallHeight,
      plinthHeight: roomPlinthHeight,
      plinthThickness: roomPlinthThickness,
    },
    walls,
    openings,
    electricalItems,
    furnitureItems,
  };
}
