import type { FastifyInstance, FastifyPluginAsync } from 'fastify';
import {
  createRoomSchema,
  updateRoomSchema,
} from '@house-plan-maker/shared';
import type {
  Room,
  RoomFull,
  Point,
  ApiResponse,
  ApiListResponse,
} from '@house-plan-maker/shared';
import {
  safeJsonParse,
  toWallResponse,
  toOpeningResponse,
  toElectricalResponse,
  toFurnitureResponse,
  toAnnotationResponse,
} from '../utils/mappers.js';

const roomRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
  const { prisma } = fastify;

  // GET /api/apartments/:id/rooms
  fastify.get<{ Params: { id: string } }>(
    '/api/apartments/:id/rooms',
    async (request, reply): Promise<ApiListResponse<Room>> => {
      const apartment = await prisma.apartment.findUnique({
        where: { id: request.params.id },
      });

      if (!apartment) {
        return reply.status(404).send({ error: 'Apartment not found', statusCode: 404 });
      }

      const rooms = await prisma.room.findMany({
        where: { apartmentId: request.params.id },
        orderBy: { order: 'asc' },
      });

      return { data: rooms.map(toRoomResponse) };
    },
  );

  // POST /api/apartments/:id/rooms
  fastify.post<{ Params: { id: string } }>(
    '/api/apartments/:id/rooms',
    async (request, reply): Promise<ApiResponse<Room>> => {
      const input = createRoomSchema.parse(request.body);

      const apartment = await prisma.apartment.findUnique({
        where: { id: request.params.id },
      });

      if (!apartment) {
        return reply.status(404).send({ error: 'Apartment not found', statusCode: 404 });
      }

      const room = await prisma.room.create({
        data: {
          apartmentId: request.params.id,
          name: input.name,
          shape: JSON.stringify(input.shape ?? []),
          width: input.width ?? null,
          height: input.height ?? null,
          wallHeight: input.wallHeight ?? 2.7,
          plinthHeight: input.plinthHeight ?? 0.06,
          plinthThickness: input.plinthThickness ?? 0.01,
          ...(input.stretchCeilingOffset !== undefined && {
            stretchCeilingOffset: input.stretchCeilingOffset,
          }),
          order: input.order ?? 0,
          // posX/posY added in schema migration; client regeneration required
          ...(input.posX !== undefined && { posX: input.posX }),
          ...(input.posY !== undefined && { posY: input.posY }),
          ...(input.floorType !== undefined && { floorType: input.floorType }),
          ...(input.wallColor !== undefined && { wallColor: input.wallColor }),
          ...(input.wallFinish !== undefined && { wallFinish: input.wallFinish }),
          ...(input.outletWidth !== undefined && { outletWidth: input.outletWidth }),
          ...(input.outletHeight !== undefined && { outletHeight: input.outletHeight }),
        } as Parameters<typeof prisma.room.create>[0]['data'],
      });

      reply.status(201);
      return { data: toRoomResponse(room) };
    },
  );

  // GET /api/rooms/:id
  fastify.get<{ Params: { id: string } }>(
    '/api/rooms/:id',
    async (request, reply): Promise<ApiResponse<Room>> => {
      const room = await prisma.room.findUnique({
        where: { id: request.params.id },
      });

      if (!room) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      return { data: toRoomResponse(room) };
    },
  );

  // GET /api/rooms/:id/full
  fastify.get<{ Params: { id: string } }>(
    '/api/rooms/:id/full',
    async (request, reply): Promise<ApiResponse<RoomFull>> => {
      const room = await prisma.room.findUnique({
        where: { id: request.params.id },
        include: {
          walls: true,
          openings: true,
          electricalItems: true,
          furnitureItems: true,
          annotations: true,
        },
      });

      if (!room) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      return {
        data: {
          ...toRoomResponse(room),
          walls: room.walls.map(toWallResponse),
          openings: room.openings.map(toOpeningResponse),
          electricalItems: room.electricalItems.map(toElectricalResponse),
          furnitureItems: room.furnitureItems.map(toFurnitureResponse),
          annotations: room.annotations.map(toAnnotationResponse),
        },
      };
    },
  );

  // PUT /api/rooms/:id
  fastify.put<{ Params: { id: string } }>(
    '/api/rooms/:id',
    async (request, reply): Promise<ApiResponse<Room>> => {
      const input = updateRoomSchema.parse(request.body);

      const existing = await prisma.room.findUnique({
        where: { id: request.params.id },
      });

      if (!existing) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      const room = await prisma.room.update({
        where: { id: request.params.id },
        data: {
          ...(input.name !== undefined && { name: input.name }),
          ...(input.shape !== undefined && { shape: JSON.stringify(input.shape) }),
          ...(input.width !== undefined && { width: input.width ?? null }),
          ...(input.height !== undefined && { height: input.height ?? null }),
          ...(input.wallHeight !== undefined && { wallHeight: input.wallHeight }),
          ...(input.plinthHeight !== undefined && { plinthHeight: input.plinthHeight }),
          ...(input.plinthThickness !== undefined && { plinthThickness: input.plinthThickness }),
          ...(input.stretchCeilingOffset !== undefined && {
            stretchCeilingOffset: input.stretchCeilingOffset,
          }),
          ...(input.order !== undefined && { order: input.order }),
          ...(input.posX !== undefined && { posX: input.posX }),
          ...(input.posY !== undefined && { posY: input.posY }),
          ...(input.floorType !== undefined && { floorType: input.floorType }),
          ...(input.wallColor !== undefined && { wallColor: input.wallColor }),
          ...(input.wallFinish !== undefined && { wallFinish: input.wallFinish }),
          ...(input.outletWidth !== undefined && { outletWidth: input.outletWidth }),
          ...(input.outletHeight !== undefined && { outletHeight: input.outletHeight }),
        },
      });

      return { data: toRoomResponse(room) };
    },
  );

  // DELETE /api/rooms/:id
  fastify.delete<{ Params: { id: string } }>(
    '/api/rooms/:id',
    async (request, reply): Promise<void> => {
      const existing = await prisma.room.findUnique({
        where: { id: request.params.id },
      });

      if (!existing) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      await prisma.room.delete({
        where: { id: request.params.id },
      });

      reply.status(204);
    },
  );

  // POST /api/rooms/:id/clone — deep-copy a room and all its contents.
  // Creates a new room in the same apartment with the same scalar properties
  // and a fresh copy of every wall, opening, electrical item, furniture item,
  // and annotation. The clone is named "<original> (copy)" and placed at the
  // end of the apartment's room order. Wall and opening IDs are regenerated
  // and remapped so the new openings reference the new walls.
  //
  // The whole copy runs inside a single Prisma transaction so a partial
  // failure leaves no orphan rows behind.
  fastify.post<{ Params: { id: string } }>(
    '/api/rooms/:id/clone',
    async (request, reply): Promise<ApiResponse<Room>> => {
      const source = await prisma.room.findUnique({
        where: { id: request.params.id },
        include: {
          walls: { include: { openings: true } },
          electricalItems: true,
          furnitureItems: true,
          annotations: true,
        },
      });

      if (!source) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      // Compute the next `order` for the apartment so the clone lands at the
      // end of the list rather than colliding with the source's order.
      const maxOrder = await prisma.room.aggregate({
        where: { apartmentId: source.apartmentId },
        _max: { order: true },
      });
      const nextOrder = (maxOrder._max.order ?? 0) + 1;

      const cloned = await prisma.$transaction(async (tx) => {
        // 1. Create the room shell with all scalar fields copied verbatim.
        const room = await tx.room.create({
          data: {
            apartmentId: source.apartmentId,
            name: `${source.name} (copy)`,
            shape: source.shape,
            width: source.width,
            height: source.height,
            wallHeight: source.wallHeight,
            plinthHeight: source.plinthHeight,
            plinthThickness: source.plinthThickness,
            stretchCeilingOffset:
              (((source as Record<string, unknown>).stretchCeilingOffset as number | undefined) ?? 0),
            order: nextOrder,
            posX: source.posX,
            posY: source.posY,
            floorType: source.floorType,
            wallColor: source.wallColor,
            wallFinish: (source as Record<string, unknown>).wallFinish ?? 'PAINT',
            outletWidth: source.outletWidth,
            outletHeight: source.outletHeight,
          } as Parameters<typeof tx.room.create>[0]['data'],
        });

        // 2. Re-create walls and remember the old→new id map so openings
        //    can be re-pointed at the cloned wall ids.
        const wallIdMap = new Map<string, string>();
        for (const w of source.walls) {
          const newWall = await tx.wall.create({
            data: {
              roomId: room.id,
              startX: w.startX,
              startY: w.startY,
              endX: w.endX,
              endY: w.endY,
              thickness: w.thickness,
              direction: w.direction,
            },
          });
          wallIdMap.set(w.id, newWall.id);
        }

        // 3. Re-create wall openings, mapping wallId through the map.
        for (const w of source.walls) {
          for (const o of w.openings) {
            const mappedWallId = wallIdMap.get(o.wallId);
            if (!mappedWallId) continue;
            await tx.wallOpening.create({
              data: {
                roomId: room.id,
                wallId: mappedWallId,
                type: o.type,
                positionAlongWall: o.positionAlongWall,
                width: o.width,
                height: o.height,
                elevationFromFloor: o.elevationFromFloor,
                openDirection: o.openDirection,
                anchorH: o.anchorH,
                anchorV: o.anchorV,
                gridCols: o.gridCols,
                gridRows: o.gridRows,
                slopeDepth: o.slopeDepth,
                frameThickness: o.frameThickness,
              },
            });
          }
        }

        // 4. Re-create electrical items. wallId points at the source wall id;
        //    re-map through wallIdMap, falling back to null if the source had
        //    no wallId (free-placed item).
        for (const e of source.electricalItems) {
          await tx.electricalItem.create({
            data: {
              roomId: room.id,
              type: e.type,
              x: e.x,
              y: e.y,
              wallId: e.wallId ? wallIdMap.get(e.wallId) ?? null : null,
              elevationFromFloor: e.elevationFromFloor,
              rotation: e.rotation,
              count: e.count,
              anchorH: e.anchorH,
              anchorV: e.anchorV,
              label: e.label,
              metadata: e.metadata,
            },
          });
        }

        // 5. Re-create furniture items.
        for (const f of source.furnitureItems) {
          await tx.furnitureItem.create({
            data: {
              roomId: room.id,
              type: f.type,
              x: f.x,
              y: f.y,
              width: f.width,
              depth: f.depth,
              height: f.height,
              rotation: f.rotation,
              elevationFromFloor: f.elevationFromFloor,
              label: f.label,
              showProjection: f.showProjection,
              opacity: f.opacity,
              anchorH: f.anchorH,
              anchorV: f.anchorV,
              metadata: f.metadata,
            },
          });
        }

        // 6. Re-create annotations. attachedToId references electrical or
        //    furniture by id; we don't have an old→new id map for those, so
        //    drop the attachment for cloned annotations rather than leave a
        //    dangling reference. Free-floating annotations are preserved.
        for (const a of source.annotations) {
          await tx.annotation.create({
            data: {
              roomId: room.id,
              x: a.x,
              y: a.y,
              text: a.text,
              fontSize: a.fontSize,
              color: a.color,
              attachedToId: null,
              projectionOffsetX: a.projectionOffsetX,
              projectionOffsetY: a.projectionOffsetY,
            },
          });
        }

        return room;
      });

      reply.status(201);
      return { data: toRoomResponse(cloned) };
    },
  );
};

function toRoomResponse(room: {
  id: string;
  apartmentId: string;
  name: string;
  shape: string;
  width: number | null;
  height: number | null;
  wallHeight: number;
  plinthHeight: number;
  plinthThickness: number;
  stretchCeilingOffset?: number | null;
  order: number;
  posX?: number | null;
  posY?: number | null;
  outletWidth?: number | null;
  outletHeight?: number | null;
  createdAt: Date;
  updatedAt: Date;
}): Room {
  return {
    id: room.id,
    apartmentId: room.apartmentId,
    name: room.name,
    shape: safeJsonParse<Point[]>(room.shape, []),
    width: room.width,
    height: room.height,
    wallHeight: room.wallHeight,
    plinthHeight: room.plinthHeight,
    plinthThickness: room.plinthThickness,
    stretchCeilingOffset: room.stretchCeilingOffset ?? 0,
    order: room.order,
    posX: room.posX ?? 0,
    posY: room.posY ?? 0,
    floorType: (((room as Record<string, unknown>).floorType as string) ?? 'CONCRETE') as Room['floorType'],
    wallColor: ((room as Record<string, unknown>).wallColor as string) ?? '#f5f0eb',
    wallFinish: (((room as Record<string, unknown>).wallFinish as string) ?? 'PAINT') as Room['wallFinish'],
    outletWidth: room.outletWidth ?? 0.07,
    outletHeight: room.outletHeight ?? 0.07,
    createdAt: room.createdAt.toISOString(),
    updatedAt: room.updatedAt.toISOString(),
  };
}

export default roomRoutes;
