/**
 * Users API adapters. Authorization (role + ownership) runs BEFORE existence
 * checks, so a non-owner always gets 403 regardless of resource existence.
 */
import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import { z } from 'zod';
import type pg from 'pg';
import { parseJson } from '../../../shared/http-input.js';
import { AppError } from '../../../shared/errors.js';
import { errorSchema } from '../../../shared/swagger.js';
import { requireOwnerOrAdmin, requireRole, type Authenticate } from '../../../shared/auth.js';
import { GetCustomer, ListCustomers, UpdateProfile } from '../application/profile-use-cases.js';
import {
  CreateAddress,
  DeleteAddress,
  ListAddresses,
  UpdateAddress,
} from '../application/address-use-cases.js';
import { PgProfileRepository } from '../infrastructure/pg-profile-repository.js';
import { PgAddressRepository } from '../infrastructure/pg-address-repository.js';
import type { Address } from '../domain/address.js';
import type { CustomerSummary, Profile } from '../domain/profile.js';

export interface UsersRoutesDeps {
  pool: pg.Pool;
  /** Injected by the composition root (identity owns session resolution). */
  authenticate: Authenticate;
}

const uuidParamSchema = z.object({ id: z.uuid() });
const addressIdParamSchema = z.object({ id: z.uuid(), addressId: z.uuid() });

const preferencesSchema = z
  .object({
    orderUpdates: z.boolean().optional(),
    newsletter: z.boolean().optional(),
    promos: z.boolean().optional(),
  })
  .strict();

const profilePatchSchema = z
  .object({
    displayName: z.string().min(1).max(200).optional(),
    phone: z.string().min(1).max(50).optional(),
    preferences: preferencesSchema.optional(),
  })
  .refine(
    (value) =>
      value.displayName !== undefined || value.phone !== undefined || value.preferences !== undefined,
    { message: 'At least one of displayName, phone or preferences is required' },
  );

const newAddressSchema = z.object({
  label: z.string().max(100).optional().nullable(),
  recipientName: z.string().min(1).max(200),
  street: z.string().min(1).max(300),
  city: z.string().min(1).max(100),
  postalCode: z.string().min(1).max(20),
  country: z.string().min(1).max(100),
  isDefault: z.boolean().optional(),
});

const addressPatchSchema = newAddressSchema
  .partial()
  .refine((value) => Object.values(value).some((field) => field !== undefined), {
    message: 'At least one address field is required',
  });

export async function registerUsersRoutes(
  app: FastifyInstance,
  deps: UsersRoutesDeps,
): Promise<void> {
  const profiles = new PgProfileRepository(deps.pool);
  const addresses = new PgAddressRepository(deps.pool);
  const getCustomer = new GetCustomer(profiles);
  const updateProfile = new UpdateProfile(profiles);
  const listCustomers = new ListCustomers(profiles);
  const listAddresses = new ListAddresses(addresses);
  const createAddress = new CreateAddress(addresses);
  const updateAddress = new UpdateAddress(addresses);
  const deleteAddress = new DeleteAddress(addresses);

  const listUsersSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'List users (admin)',
    querystring: {
      type: 'object',
      properties: {
        q: { type: 'string' },
        limit: { type: 'integer', default: 20 },
        offset: { type: 'integer', default: 0 },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/users', { schema: listUsersSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const query = request.query as Record<string, string | undefined>;
    const q = query.q || undefined;
    const offset = Math.max(0, parseInt(query.offset ?? '0', 10) || 0);
    const limit = Math.min(100, Math.max(1, parseInt(query.limit ?? '20', 10) || 20));
    const result = await listCustomers.execute({ q, offset, limit });
    return reply.send({
      items: result.items.map(serializeCustomer),
      total: result.total,
    });
  });

  const getUserSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'Get user',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.get('/users/:id', { schema: getUserSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const { id } = parseJson(uuidParamSchema, request.params);
    requireOwnerOrAdmin(user, id);
    const customer = await getCustomer.execute(id);
    if (!customer) {
      throw new AppError(404, 'NOT_FOUND', 'Customer not found');
    }
    const profile = await profiles.findByUserId(id);
    return reply.send({ ...serializeCustomer(customer), preferences: profile?.preferences ?? undefined });
  });

  const patchUserSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'Update user profile',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch('/users/:id', { schema: patchUserSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const { id } = parseJson(uuidParamSchema, request.params);
    requireOwnerOrAdmin(user, id);
    const patch = parseJson(profilePatchSchema, request.body);
    const profile = await updateProfile.execute(id, patch);
    return reply.send(serializeProfile(profile));
  });

  const addressesSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'List user addresses',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/users/:id/addresses', { schema: addressesSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const { id } = parseJson(uuidParamSchema, request.params);
    requireOwnerOrAdmin(user, id);
    const items = await listAddresses.execute(id);
    return reply.send({ items: items.map(serializeAddress) });
  });

  const createAddressSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'Create address',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/users/:id/addresses', { schema: createAddressSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    const { id } = parseJson(uuidParamSchema, request.params);
    requireOwnerOrAdmin(user, id);
    const input = parseJson(newAddressSchema, request.body);
    const address = await createAddress.execute(id, input);
    return reply.code(201).send(serializeAddress(address));
  });

  const updateAddressSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'Update address',
    params: {
      type: 'object',
      required: ['id', 'addressId'],
      properties: {
        id: { type: 'string', format: 'uuid' },
        addressId: { type: 'string', format: 'uuid' },
      },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.patch(
    '/users/:id/addresses/:addressId',
    { schema: updateAddressSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      const { id, addressId } = parseJson(addressIdParamSchema, request.params);
      requireOwnerOrAdmin(user, id);
      const patch = parseJson(addressPatchSchema, request.body);
      const address = await updateAddress.execute(id, addressId, patch);
      if (!address) {
        throw new AppError(404, 'NOT_FOUND', 'Address not found');
      }
      return reply.send(serializeAddress(address));
    },
  );

  const deleteAddressSchema: FastifySchema = {
    tags: ['Users'],
    summary: 'Delete address',
    params: {
      type: 'object',
      required: ['id', 'addressId'],
      properties: {
        id: { type: 'string', format: 'uuid' },
        addressId: { type: 'string', format: 'uuid' },
      },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema, 404: errorSchema },
  };
  app.delete(
    '/users/:id/addresses/:addressId',
    { schema: deleteAddressSchema },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      const { id, addressId } = parseJson(addressIdParamSchema, request.params);
      requireOwnerOrAdmin(user, id);
      const deleted = await deleteAddress.execute(id, addressId);
      if (!deleted) {
        throw new AppError(404, 'NOT_FOUND', 'Address not found');
      }
      return reply.code(204).send();
    },
  );
}

function serializeProfile(profile: Profile) {
  return {
    userId: profile.userId,
    displayName: profile.displayName,
    phone: profile.phone,
    preferences: profile.preferences,
    createdAt: profile.createdAt.toISOString(),
    updatedAt: profile.updatedAt.toISOString(),
  };
}

function serializeCustomer(c: CustomerSummary) {
  return {
    id: c.userId,
    email: c.email,
    role: c.role,
    displayName: c.displayName,
    phone: c.phone,
    createdAt: c.createdAt.toISOString(),
  };
}

function serializeAddress(address: Address) {
  return {
    id: address.id,
    userId: address.userId,
    label: address.label,
    recipientName: address.recipientName,
    street: address.street,
    city: address.city,
    postalCode: address.postalCode,
    country: address.country,
    isDefault: address.isDefault,
    createdAt: address.createdAt.toISOString(),
    updatedAt: address.updatedAt.toISOString(),
  };
}
