import fp from 'fastify-plugin';
import { asClass, Lifetime } from 'awilix';
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
import type { WebSocket } from 'ws';
import {
  CreateShoppingListSchema,
  UpdateShoppingListSchema,
  AddShoppingItemSchema,
  UpdateShoppingItemSchema,
  ShoppingListResponseSchema,
  ShoppingListSyncToPantryResponseSchema,
  BasketStoreComparisonResponseSchema,
  type ShoppingItem,
} from '@meshitrack/shared';
import { ShoppingListsRepository } from './shopping-lists.repository.js';
import { ShoppingListsService } from './shopping-lists.service.js';
import { ShoppingGapService } from '../meal-plans/shopping-gap.service.js';
import { PantryService } from '../pantry/pantry.service.js';
import { ProductsRepository } from '../products/products.repository.js';
import { PricesService } from '../prices/prices.service.js';
import { MealPlanRepository } from '../meal-plans/meal-plans.repository.js';
import { PantryRepository } from '../pantry/pantry.repository.js';
import { RecipesRepository } from '../recipes/recipes.repository.js';
import { StoresRepository } from '../stores/stores.repository.js';
import { PricesRepository } from '../prices/prices.repository.js';

// Memory track for live concurrent websocket clients per active list session
const activeListSockets = new Map<string, Set<WebSocket>>();

/* v8 ignore start */
function broadcastToList(listId: string, excludeSocket: WebSocket, message: any) {
  const set = activeListSockets.get(listId);
  if (!set) return;
  const payload = JSON.stringify(message);
  for (const socket of set) {
    if (socket !== excludeSocket && socket.readyState === 1 /* OPEN */) {
      socket.send(payload);
    }
  }
}
/* v8 ignore stop */

declare module '@fastify/awilix' {
  interface Cradle {
    shoppingListsRepository: ShoppingListsRepository;
    shoppingListsService: ShoppingListsService;
  }
}

function serializeList(doc: any) {
  return {
    ...doc,
    _id: doc._id.toString(),
    createdAt: doc.createdAt?.toISOString(),
    updatedAt: doc.updatedAt?.toISOString(),
    completedAt: doc.completedAt?.toISOString(),
    items: doc.items.map((it: any) => ({
      ...it,
      checkedAt: it.checkedAt?.toISOString(),
    })),
  };
}

export default fp(
  async (fastify) => {
    // Register Awilix classes
    fastify.diContainer.register({
      shoppingListsRepository: asClass(ShoppingListsRepository, { lifetime: Lifetime.SINGLETON }),
      shoppingListsService: asClass(ShoppingListsService, { lifetime: Lifetime.SINGLETON }),

      // Cross-domain requirements to service the list workflow orchestrations
      shoppingGapService: asClass(ShoppingGapService, { lifetime: Lifetime.SINGLETON }),
      pantryService: asClass(PantryService, { lifetime: Lifetime.SINGLETON }),
      pantryRepository: asClass(PantryRepository, { lifetime: Lifetime.SINGLETON }),
      productsRepository: asClass(ProductsRepository, { lifetime: Lifetime.SINGLETON }),
      pricesService: asClass(PricesService, { lifetime: Lifetime.SINGLETON }),
      pricesRepository: asClass(PricesRepository, { lifetime: Lifetime.SINGLETON }),
      mealPlanRepository: asClass(MealPlanRepository, { lifetime: Lifetime.SINGLETON }),
      recipesRepository: asClass(RecipesRepository, { lifetime: Lifetime.SINGLETON }),
      storesRepository: asClass(StoresRepository, { lifetime: Lifetime.SINGLETON }),
    });

    const app = fastify.withTypeProvider<ZodTypeProvider>();
    const householdParams = z.object({ householdId: z.string() });
    const listIdParams = householdParams.extend({ id: z.string() });

    // 1. Standard List CRUD

    app.route({
      method: 'GET',
      url: '/api/v1/households/:householdId/shopping-lists',
      schema: {
        params: householdParams,
        response: { 200: z.array(ShoppingListResponseSchema) },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const lists = await service.list(request.params.householdId);
        return reply.send(lists.map(serializeList));
      },
    });

    app.route({
      method: 'POST',
      url: '/api/v1/households/:householdId/shopping-lists',
      schema: {
        params: householdParams,
        body: CreateShoppingListSchema,
        response: { 201: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const list = await service.create(
          request.body,
          request.params.householdId,
          request.user.keycloakId,
        );
        return reply.status(201).send(serializeList(list));
      },
    });

    app.route({
      method: 'GET',
      url: '/api/v1/households/:householdId/shopping-lists/:id',
      schema: {
        params: listIdParams,
        response: { 200: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const list = await service.getById(request.params.id, request.params.householdId);
        return reply.send(serializeList(list));
      },
    });

    app.route({
      method: 'PATCH',
      url: '/api/v1/households/:householdId/shopping-lists/:id',
      schema: {
        params: listIdParams,
        body: UpdateShoppingListSchema,
        response: { 200: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const list = await service.update(
          request.params.id,
          request.params.householdId,
          request.body,
        );
        return reply.send(serializeList(list));
      },
    });

    app.route({
      method: 'DELETE',
      url: '/api/v1/households/:householdId/shopping-lists/:id',
      schema: {
        params: listIdParams,
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        await service.delete(request.params.id, request.params.householdId);
        return reply.status(204).send();
      },
    });

    // 2. Nested Item Modifications

    app.route({
      method: 'POST',
      url: '/api/v1/households/:householdId/shopping-lists/:id/items',
      schema: {
        params: listIdParams,
        body: AddShoppingItemSchema,
        response: { 201: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const { list, addedItem } = await service.addItem(
          request.params.id,
          request.params.householdId,
          request.body,
        );

        // Emit real-time update notification to existing connected viewers
        broadcastToList(request.params.id, null as any, {
          type: 'ITEM_ADDED',
          item: { ...addedItem, checkedAt: undefined },
        });

        return reply.status(201).send(serializeList(list));
      },
    });

    app.route({
      method: 'PATCH',
      url: '/api/v1/households/:householdId/shopping-lists/:id/items/:itemId',
      schema: {
        params: listIdParams.extend({ itemId: z.string() }),
        body: UpdateShoppingItemSchema,
        response: { 200: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const updatedList = await service.updateItem(
          request.params.id,
          request.params.householdId,
          request.params.itemId,
          request.body,
          request.user.keycloakId,
        );

        // Broadcast the precise item differential state update to sibling websocket listeners
        const matchedItem = updatedList.items.find(
          (i: ShoppingItem) => i.id === request.params.itemId,
        );
        if (matchedItem) {
          broadcastToList(request.params.id, null as any, {
            type: 'ITEM_UPDATED',
            itemId: request.params.itemId,
            updates: {
              ...request.body,
              checkedAt: matchedItem.checkedAt?.toISOString(),
              checkedBy: matchedItem.checkedBy,
            },
          });
        }

        return reply.send(serializeList(updatedList));
      },
    });

    app.route({
      method: 'DELETE',
      url: '/api/v1/households/:householdId/shopping-lists/:id/items/:itemId',
      schema: {
        params: listIdParams.extend({ itemId: z.string() }),
        response: { 200: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const list = await service.removeItem(
          request.params.id,
          request.params.householdId,
          request.params.itemId,
        );

        broadcastToList(request.params.id, null as any, {
          type: 'ITEM_REMOVED',
          itemId: request.params.itemId,
        });

        return reply.send(serializeList(list));
      },
    });

    // 3. Domain Workflow Hooks

    app.route({
      method: 'POST',
      url: '/api/v1/households/:householdId/shopping-lists/from-meal-plan/:mealPlanId',
      schema: {
        params: householdParams.extend({ mealPlanId: z.string() }),
        response: { 201: ShoppingListResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const list = await service.createFromMealPlan(
          request.params.mealPlanId,
          request.params.householdId,
          request.user.keycloakId,
        );
        return reply.status(201).send(serializeList(list));
      },
    });

    app.route({
      method: 'POST',
      url: '/api/v1/households/:householdId/shopping-lists/:id/sync-to-pantry',
      schema: {
        params: listIdParams,
        response: { 200: ShoppingListSyncToPantryResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const results = await service.syncCheckedToPantry(
          request.params.id,
          request.params.householdId,
          request.user.keycloakId,
        );
        return reply.send(results);
      },
    });

    app.route({
      method: 'GET',
      url: '/api/v1/households/:householdId/shopping-lists/:id/stores',
      schema: {
        params: listIdParams,
        response: { 200: BasketStoreComparisonResponseSchema },
      },
      handler: async (request, reply) => {
        const service = fastify.diContainer.resolve('shoppingListsService');
        const comparison = await service.getStoreComparison(
          request.params.id,
          request.params.householdId,
        );
        return reply.send(comparison);
      },
    });

    // 4. Persist Collaborative WebSocket Handshakes

    /* v8 ignore start */
    app.get(
      '/api/v1/households/:householdId/shopping-lists/:id/sync',
      { websocket: true },
      (connection: any, request: any) => {
        const socket = connection.socket;
        const listId = request.params.id;

        // Setup connection context
        if (!activeListSockets.has(listId)) {
          activeListSockets.set(listId, new Set());
        }
        activeListSockets.get(listId)!.add(socket);

        request.log.info({ listId }, 'Active client joined shopping list sync channel');

        socket.on('message', async (messageBuffer: any) => {
          try {
            const payload = JSON.parse(messageBuffer.toString());

            // Handlers for inbound events e.g. real-time toggle checks from frontends
            if (payload.type === 'TOGGLE_ITEM') {
              const service = fastify.diContainer.resolve('shoppingListsService');
              const updatedList = await service.updateItem(
                listId,
                request.params.householdId,
                payload.itemId,
                { checked: payload.checked },
                request.user.keycloakId,
              );

              const matched = updatedList.items.find(
                (it: ShoppingItem) => it.id === payload.itemId,
              );

              // Echo back differential confirmation to everyone else on the floor
              broadcastToList(listId, socket, {
                type: 'ITEM_UPDATED',
                itemId: payload.itemId,
                updates: {
                  checked: payload.checked,
                  checkedAt: matched?.checkedAt?.toISOString(),
                  checkedBy: matched?.checkedBy,
                },
              });
            }
          } catch (err) {
            request.log.error(err, 'WebSocket message sync processing error');
          }
        });

        socket.on('close', () => {
          const set = activeListSockets.get(listId);
          if (set) {
            set.delete(socket);
            if (set.size === 0) {
              activeListSockets.delete(listId);
            }
          }
          request.log.info({ listId }, 'Client severed sync handshake connection');
        });
      },
    );
    /* v8 ignore stop */
  },
  {
    name: 'shopping-lists-routes',
    dependencies: ['auth-plugin'],
  },
);
