import type { FastifyInstance, FastifyPluginAsync } from 'fastify';
import {
  bulkUpdateWallsSchema,
  createWallOpeningSchema,
  updateWallOpeningSchema,
  createElectricalItemSchema,
  updateElectricalItemSchema,
  createFurnitureItemSchema,
  updateFurnitureItemSchema,
  batchSyncOpeningsSchema,
  batchSyncElectricalSchema,
  batchSyncFurnitureSchema,
  batchSyncAnnotationsSchema,
} from '@house-plan-maker/shared';
import type {
  Wall,
  WallOpening,
  ElectricalItem,
  FurnitureItem,
  Annotation,
  ApiResponse,
  ApiListResponse,
} from '@house-plan-maker/shared';
import {
  toWallResponse,
  toOpeningResponse,
  toElectricalResponse,
  toFurnitureResponse,
  toAnnotationResponse,
} from '../utils/mappers.js';

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

  // ── Walls ──

  // PUT /api/rooms/:id/walls — bulk replace all walls in a room
  fastify.put<{ Params: { id: string } }>(
    '/api/rooms/:id/walls',
    async (request, reply): Promise<ApiResponse<readonly Wall[]>> => {
      const input = bulkUpdateWallsSchema.parse(request.body);

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

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

      const walls = await prisma.$transaction(async (tx) => {
        await tx.wall.deleteMany({ where: { roomId: request.params.id } });

        const created = await Promise.all(
          input.walls.map((w) =>
            tx.wall.create({
              data: {
                roomId: request.params.id,
                startX: w.startX,
                startY: w.startY,
                endX: w.endX,
                endY: w.endY,
                thickness: w.thickness ?? 0.1,
                direction: w.direction ?? 'OTHER',
              },
            }),
          ),
        );

        return created;
      });

      return { data: walls.map(toWallResponse) };
    },
  );

  // ── Wall Openings ──

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

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

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

      const wall = await prisma.wall.findUnique({
        where: { id: input.wallId },
      });

      if (!wall || wall.roomId !== request.params.id) {
        return reply.status(400).send({ error: 'Wall not found in this room', statusCode: 400 });
      }

      const opening = await prisma.wallOpening.create({
        data: {
          roomId: request.params.id,
          wallId: input.wallId,
          type: input.type,
          positionAlongWall: input.positionAlongWall,
          width: input.width,
          height: input.height,
          elevationFromFloor: input.elevationFromFloor ?? 0,
          openDirection: input.openDirection ?? 'LEFT',
          // Openings store canonical (positionAlongWall = center, elevationFromFloor = bottom).
          // PositionAnchor on openings is a view-only preference used by the
          // properties panel; renderers always read canonical values.
          anchorH: input.positionAnchor?.horizontal ?? 'middle',
          anchorV: input.positionAnchor?.vertical ?? 'bottom',
          gridCols: input.gridCols ?? 2,
          gridRows: input.gridRows ?? 2,
          slopeDepth: input.slopeDepth ?? 0,
          frameThickness: input.frameThickness ?? 0.03,
        },
      });

      reply.status(201);
      return { data: toOpeningResponse(opening) };
    },
  );

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

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

      if (!existing || existing.roomId !== request.params.id) {
        return reply.status(404).send({ error: 'Opening not found', statusCode: 404 });
      }

      const opening = await prisma.wallOpening.update({
        where: { id: request.params.openingId },
        data: {
          ...(input.type !== undefined && { type: input.type }),
          ...(input.positionAlongWall !== undefined && { positionAlongWall: input.positionAlongWall }),
          ...(input.width !== undefined && { width: input.width }),
          ...(input.height !== undefined && { height: input.height }),
          ...(input.elevationFromFloor !== undefined && { elevationFromFloor: input.elevationFromFloor }),
          ...(input.openDirection !== undefined && { openDirection: input.openDirection }),
          ...(input.positionAnchor !== undefined && {
            anchorH: input.positionAnchor.horizontal,
            anchorV: input.positionAnchor.vertical,
          }),
          ...(input.gridCols !== undefined && { gridCols: input.gridCols }),
          ...(input.gridRows !== undefined && { gridRows: input.gridRows }),
          ...(input.slopeDepth !== undefined && { slopeDepth: input.slopeDepth }),
          ...(input.frameThickness !== undefined && { frameThickness: input.frameThickness }),
        },
      });

      return { data: toOpeningResponse(opening) };
    },
  );

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

      if (!existing || existing.roomId !== request.params.id) {
        return reply.status(404).send({ error: 'Opening not found', statusCode: 404 });
      }

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

      reply.status(204);
    },
  );

  // ── Electrical Items ──

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

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

      const items = await prisma.electricalItem.findMany({
        where: { roomId: request.params.id },
      });

      return { data: items.map(toElectricalResponse) };
    },
  );

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

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

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

      const item = await prisma.electricalItem.create({
        data: {
          roomId: request.params.id,
          type: input.type,
          x: input.x,
          y: input.y,
          wallId: input.wallId ?? null,
          elevationFromFloor: input.elevationFromFloor ?? null,
          rotation: input.rotation ?? 0,
          count: Math.max(1, Math.round(input.count ?? 1)),
          anchorH: input.positionAnchor?.horizontal ?? 'middle',
          anchorV: input.positionAnchor?.vertical ?? 'middle',
          label: input.label ?? null,
          metadata: input.metadata ? JSON.stringify(input.metadata) : null,
        },
      });

      reply.status(201);
      return { data: toElectricalResponse(item) };
    },
  );

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

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

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

      const item = await prisma.electricalItem.update({
        where: { id: request.params.id },
        data: {
          ...(input.type !== undefined && { type: input.type }),
          ...(input.x !== undefined && { x: input.x }),
          ...(input.y !== undefined && { y: input.y }),
          ...(input.wallId !== undefined && { wallId: input.wallId ?? null }),
          ...(input.elevationFromFloor !== undefined && { elevationFromFloor: input.elevationFromFloor ?? null }),
          ...(input.rotation !== undefined && { rotation: input.rotation }),
          ...(input.count !== undefined && { count: Math.max(1, Math.round(input.count)) }),
          ...(input.positionAnchor !== undefined && {
            anchorH: input.positionAnchor.horizontal,
            anchorV: input.positionAnchor.vertical,
          }),
          ...(input.label !== undefined && { label: input.label ?? null }),
          ...(input.metadata !== undefined && {
            metadata: input.metadata ? JSON.stringify(input.metadata) : null,
          }),
        },
      });

      return { data: toElectricalResponse(item) };
    },
  );

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

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

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

      reply.status(204);
    },
  );

  // ── Furniture Items ──

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

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

      const items = await prisma.furnitureItem.findMany({
        where: { roomId: request.params.id },
      });

      return { data: items.map(toFurnitureResponse) };
    },
  );

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

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

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

      const item = await prisma.furnitureItem.create({
        data: {
          roomId: request.params.id,
          type: input.type,
          x: input.x,
          y: input.y,
          width: input.width,
          depth: input.depth,
          height: input.height,
          rotation: input.rotation ?? 0,
          elevationFromFloor: input.elevationFromFloor ?? 0,
          label: input.label ?? null,
          anchorH: input.positionAnchor?.horizontal ?? 'middle',
          anchorV: input.positionAnchor?.vertical ?? 'middle',
          metadata: input.metadata ? JSON.stringify(input.metadata) : null,
        },
      });

      reply.status(201);
      return { data: toFurnitureResponse(item) };
    },
  );

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

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

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

      const item = await prisma.furnitureItem.update({
        where: { id: request.params.id },
        data: {
          ...(input.type !== undefined && { type: input.type }),
          ...(input.x !== undefined && { x: input.x }),
          ...(input.y !== undefined && { y: input.y }),
          ...(input.width !== undefined && { width: input.width }),
          ...(input.depth !== undefined && { depth: input.depth }),
          ...(input.height !== undefined && { height: input.height }),
          ...(input.rotation !== undefined && { rotation: input.rotation }),
          ...(input.elevationFromFloor !== undefined && { elevationFromFloor: input.elevationFromFloor }),
          ...(input.label !== undefined && { label: input.label ?? null }),
          ...(input.positionAnchor !== undefined && {
            anchorH: input.positionAnchor.horizontal,
            anchorV: input.positionAnchor.vertical,
          }),
          ...(input.metadata !== undefined && {
            metadata: input.metadata ? JSON.stringify(input.metadata) : null,
          }),
        },
      });

      return { data: toFurnitureResponse(item) };
    },
  );

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

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

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

      reply.status(204);
    },
  );
  // ── Batch Sync: Openings ──

  fastify.put<{ Params: { id: string } }>(
    '/api/rooms/:id/openings/batch',
    async (request, reply): Promise<ApiListResponse<WallOpening>> => {
      const input = batchSyncOpeningsSchema.parse(request.body);
      const roomId = request.params.id;

      const room = await prisma.room.findUnique({ where: { id: roomId } });
      if (!room) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      await prisma.$transaction(async (tx) => {
        // Deletes
        if (input.delete.length > 0) {
          await tx.wallOpening.deleteMany({
            where: { id: { in: input.delete }, roomId },
          });
        }

        // Updates
        for (const item of input.update) {
          await tx.wallOpening.update({
            where: { id: item.id },
            data: {
              ...(item.data.type !== undefined && { type: item.data.type }),
              ...(item.data.positionAlongWall !== undefined && { positionAlongWall: item.data.positionAlongWall }),
              ...(item.data.width !== undefined && { width: item.data.width }),
              ...(item.data.height !== undefined && { height: item.data.height }),
              ...(item.data.elevationFromFloor !== undefined && { elevationFromFloor: item.data.elevationFromFloor }),
              ...(item.data.openDirection !== undefined && { openDirection: item.data.openDirection }),
              ...(item.data.positionAnchor !== undefined && {
                anchorH: item.data.positionAnchor.horizontal,
                anchorV: item.data.positionAnchor.vertical,
              }),
              ...(item.data.gridCols !== undefined && { gridCols: item.data.gridCols }),
              ...(item.data.gridRows !== undefined && { gridRows: item.data.gridRows }),
              ...(item.data.slopeDepth !== undefined && { slopeDepth: item.data.slopeDepth }),
              ...(item.data.frameThickness !== undefined && { frameThickness: item.data.frameThickness }),
            },
          });
        }

        // Creates
        for (const item of input.create) {
          await tx.wallOpening.create({
            data: {
              roomId,
              wallId: item.wallId,
              type: item.type,
              positionAlongWall: item.positionAlongWall,
              width: item.width,
              height: item.height,
              elevationFromFloor: item.elevationFromFloor ?? 0,
              openDirection: item.openDirection ?? 'LEFT',
              anchorH: item.positionAnchor?.horizontal ?? 'middle',
              anchorV: item.positionAnchor?.vertical ?? 'bottom',
              gridCols: item.gridCols ?? 2,
              gridRows: item.gridRows ?? 2,
              slopeDepth: item.slopeDepth ?? 0,
              frameThickness: item.frameThickness ?? 0.03,
            },
          });
        }
      });

      const allOpenings = await prisma.wallOpening.findMany({ where: { roomId } });
      return { data: allOpenings.map(toOpeningResponse) };
    },
  );

  // ── Batch Sync: Electrical ──

  fastify.put<{ Params: { id: string } }>(
    '/api/rooms/:id/electrical/batch',
    async (request, reply): Promise<ApiListResponse<ElectricalItem>> => {
      const input = batchSyncElectricalSchema.parse(request.body);
      const roomId = request.params.id;

      const room = await prisma.room.findUnique({ where: { id: roomId } });
      if (!room) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      await prisma.$transaction(async (tx) => {
        // Deletes
        if (input.delete.length > 0) {
          await tx.electricalItem.deleteMany({
            where: { id: { in: input.delete }, roomId },
          });
        }

        // Updates
        for (const item of input.update) {
          await tx.electricalItem.update({
            where: { id: item.id },
            data: {
              ...(item.data.type !== undefined && { type: item.data.type }),
              ...(item.data.x !== undefined && { x: item.data.x }),
              ...(item.data.y !== undefined && { y: item.data.y }),
              ...(item.data.wallId !== undefined && { wallId: item.data.wallId ?? null }),
              ...(item.data.elevationFromFloor !== undefined && { elevationFromFloor: item.data.elevationFromFloor ?? null }),
              ...(item.data.rotation !== undefined && { rotation: item.data.rotation }),
              ...(item.data.count !== undefined && { count: Math.max(1, Math.round(item.data.count)) }),
              ...(item.data.positionAnchor !== undefined && {
                anchorH: item.data.positionAnchor.horizontal,
                anchorV: item.data.positionAnchor.vertical,
              }),
              ...(item.data.label !== undefined && { label: item.data.label ?? null }),
              ...(item.data.metadata !== undefined && {
                metadata: item.data.metadata ? JSON.stringify(item.data.metadata) : null,
              }),
            },
          });
        }

        // Creates
        for (const item of input.create) {
          await tx.electricalItem.create({
            data: {
              roomId,
              type: item.type,
              x: item.x,
              y: item.y,
              wallId: item.wallId ?? null,
              elevationFromFloor: item.elevationFromFloor ?? null,
              rotation: item.rotation ?? 0,
              count: Math.max(1, Math.round(item.count ?? 1)),
              anchorH: item.positionAnchor?.horizontal ?? 'middle',
              anchorV: item.positionAnchor?.vertical ?? 'middle',
              label: item.label ?? null,
              metadata: item.metadata ? JSON.stringify(item.metadata) : null,
            },
          });
        }
      });

      const allItems = await prisma.electricalItem.findMany({ where: { roomId } });
      return { data: allItems.map(toElectricalResponse) };
    },
  );

  // ── Batch Sync: Furniture ──

  fastify.put<{ Params: { id: string } }>(
    '/api/rooms/:id/furniture/batch',
    async (request, reply): Promise<ApiListResponse<FurnitureItem>> => {
      const input = batchSyncFurnitureSchema.parse(request.body);
      const roomId = request.params.id;

      const room = await prisma.room.findUnique({ where: { id: roomId } });
      if (!room) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      await prisma.$transaction(async (tx) => {
        // Deletes
        if (input.delete.length > 0) {
          await tx.furnitureItem.deleteMany({
            where: { id: { in: input.delete }, roomId },
          });
        }

        // Updates
        for (const item of input.update) {
          await tx.furnitureItem.update({
            where: { id: item.id },
            data: {
              ...(item.data.type !== undefined && { type: item.data.type }),
              ...(item.data.x !== undefined && { x: item.data.x }),
              ...(item.data.y !== undefined && { y: item.data.y }),
              ...(item.data.width !== undefined && { width: item.data.width }),
              ...(item.data.depth !== undefined && { depth: item.data.depth }),
              ...(item.data.height !== undefined && { height: item.data.height }),
              ...(item.data.rotation !== undefined && { rotation: item.data.rotation }),
              ...(item.data.elevationFromFloor !== undefined && { elevationFromFloor: item.data.elevationFromFloor }),
              ...(item.data.label !== undefined && { label: item.data.label ?? null }),
              ...(item.data.showProjection !== undefined && { showProjection: item.data.showProjection }),
              ...(item.data.opacity !== undefined && { opacity: item.data.opacity }),
              ...(item.data.positionAnchor !== undefined && {
                anchorH: item.data.positionAnchor.horizontal,
                anchorV: item.data.positionAnchor.vertical,
              }),
              ...(item.data.metadata !== undefined && {
                metadata: item.data.metadata ? JSON.stringify(item.data.metadata) : null,
              }),
            },
          });
        }

        // Creates
        for (const item of input.create) {
          await tx.furnitureItem.create({
            data: {
              roomId,
              type: item.type,
              x: item.x,
              y: item.y,
              width: item.width,
              depth: item.depth,
              height: item.height,
              rotation: item.rotation ?? 0,
              elevationFromFloor: item.elevationFromFloor ?? 0,
              label: item.label ?? null,
              showProjection: item.showProjection ?? false,
              opacity: item.opacity ?? 1,
              anchorH: item.positionAnchor?.horizontal ?? 'middle',
              anchorV: item.positionAnchor?.vertical ?? 'middle',
              metadata: item.metadata ? JSON.stringify(item.metadata) : null,
            },
          });
        }
      });

      const allItems = await prisma.furnitureItem.findMany({ where: { roomId } });
      return { data: allItems.map(toFurnitureResponse) };
    },
  );

  // ── Batch Sync: Annotations ──

  fastify.put<{ Params: { id: string } }>(
    '/api/rooms/:id/annotations/batch',
    async (request, reply): Promise<ApiListResponse<Annotation>> => {
      const input = batchSyncAnnotationsSchema.parse(request.body);
      const roomId = request.params.id;

      const room = await prisma.room.findUnique({ where: { id: roomId } });
      if (!room) {
        return reply.status(404).send({ error: 'Room not found', statusCode: 404 });
      }

      // SQLite is single-writer; nesting transactional calls under
      // `$transaction(async tx => ...)` deadlocks against the outer client.
      // Run the operations sequentially without a wrapping interactive
      // transaction — each call is atomic on its own.
      const annotationModel = prisma.annotation;

      if (input.delete.length > 0) {
        await annotationModel.deleteMany({
          where: { id: { in: [...input.delete] }, roomId },
        });
      }

      for (const item of input.update) {
        // Skip ids that don't exist (e.g. an annotation already deleted by
        // another tab). updateMany silently no-ops on missing rows.
        await annotationModel.updateMany({
          where: { id: item.id, roomId },
          data: {
            ...(item.data.x !== undefined && { x: item.data.x }),
            ...(item.data.y !== undefined && { y: item.data.y }),
            ...(item.data.text !== undefined && { text: item.data.text }),
            ...(item.data.fontSize !== undefined && { fontSize: item.data.fontSize ?? null }),
            ...(item.data.color !== undefined && { color: item.data.color ?? null }),
            ...(item.data.attachedToId !== undefined && { attachedToId: item.data.attachedToId ?? null }),
            ...(item.data.projectionOffsetX !== undefined && { projectionOffsetX: item.data.projectionOffsetX ?? null }),
            ...(item.data.projectionOffsetY !== undefined && { projectionOffsetY: item.data.projectionOffsetY ?? null }),
          },
        });
      }

      for (const item of input.create) {
        await annotationModel.create({
          data: {
            roomId,
            x: item.x,
            y: item.y,
            text: item.text,
            fontSize: item.fontSize ?? null,
            color: item.color ?? null,
            attachedToId: item.attachedToId ?? null,
            projectionOffsetX: item.projectionOffsetX ?? null,
            projectionOffsetY: item.projectionOffsetY ?? null,
          },
        });
      }

      const allItems = await annotationModel.findMany({ where: { roomId } });
      return { data: allItems.map(toAnnotationResponse) };
    },
  );
};

export default elementRoutes;
