import type { FastifyInstance, FastifyReply, FastifyRequest, FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import type { Authenticate, CurrentUser } from '../../../shared/auth.js';
import { requireRole } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { parseJson } from '../../../shared/http-input.js';
import { errorSchema } from '../../../shared/swagger.js';
import { ClubService } from '../application/club-service.js';
import type { ClubMember, ClubRecoveryCode, ClubSettings, ClubTransaction } from '../domain/club.js';
import {
  ClubAnonymousJoinDisabledError,
  ClubDeviceTokenRequiredError,
  ClubDisabledError,
  ClubInsufficientBalanceError,
  ClubMemberNotFoundError,
  ClubRecoveryCodeInvalidError,
  ClubRecoveryCodesDisabledError,
  InvalidClubTransactionError,
} from '../domain/errors.js';
import { PgClubRepository } from '../infrastructure/pg-club-repository.js';

export const CLUB_DEVICE_COOKIE_NAME = 'mdv_club';
const CLUB_DEVICE_COOKIE_MAX_AGE_S = 60 * 60 * 24 * 365;

export interface ClubRoutesDeps {
  pool: pg.Pool;
  authenticate: Authenticate;
}

const adminSettingsSchema = z
  .object({
    clubEnabled: z.boolean().optional(),
    cashbackPercentage: z.number().min(0).max(100).optional(),
    allowAnonymousMembers: z.boolean().optional(),
    allowRecoveryCodes: z.boolean().optional(),
    minimumRedeemAmountCents: z.number().int().min(0).optional(),
  })
  .strip();

const movementQuerySchema = z.object({
  limit: z.coerce.number().int().min(1).max(200).default(50),
});

export async function registerClubRoutes(
  app: FastifyInstance,
  deps: ClubRoutesDeps,
): Promise<void> {
  const clubs = new ClubService(new PgClubRepository(deps.pool));

  app.get(
    '/club/config',
    {
      schema: {
        tags: ['Club'],
        summary: 'Get public Club configuration',
        response: { 200: { type: 'object' } },
      } as FastifySchema,
    },
    async (_request, reply) => {
      return reply.send(serializeSettings(await clubs.getPublicConfig()));
    },
  );

  app.post(
    '/club/join',
    {
      schema: {
        tags: ['Club'],
        summary: 'Create or reuse anonymous Club member for the current device',
        response: { 201: { type: 'object' }, 200: { type: 'object' }, 409: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      try {
        const result = await clubs.joinAnonymous(deviceTokenFromRequest(request));
        setClubDeviceCookie(reply, result.deviceToken, isSecureRequest(request));
        const config = await clubs.getPublicConfig();
        return reply.code(result.created ? 201 : 200).send({
          created: result.created,
          deviceToken: result.deviceToken,
          member: serializeMember(result.member),
          config: serializeSettings(config),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  app.get(
    '/club/me',
    {
      schema: {
        tags: ['Club'],
        summary: 'Get current Club member by device token or linked account',
        response: { 200: { type: 'object' }, 401: errorSchema, 404: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await authenticateOptional(deps.authenticate, request);
      try {
        const member = await clubs.getMemberOrThrow({
          userId: user?.role === 'customer' ? user.id : null,
          deviceToken: deviceTokenFromRequest(request),
        });
        const config = await clubs.getPublicConfig();
        return reply.send({ member: serializeMember(member), config: serializeSettings(config) });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  app.get(
    '/club/movements',
    {
      schema: {
        tags: ['Club'],
        summary: 'List Club ledger movements for current member',
        querystring: {
          type: 'object',
          properties: { limit: { type: 'integer', minimum: 1, maximum: 200, default: 50 } },
        },
        response: { 200: { type: 'object' }, 401: errorSchema, 404: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await authenticateOptional(deps.authenticate, request);
      const { limit } = parseJson(movementQuerySchema, request.query ?? {});
      try {
        const { member, items } = await clubs.listMovements(
          {
            userId: user?.role === 'customer' ? user.id : null,
            deviceToken: deviceTokenFromRequest(request),
          },
          limit,
        );
        return reply.send({
          member: serializeMember(member),
          items: items.map(serializeTransaction),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  // ── Link anonymous member to registered user account ──────────────────────
  // Called by the frontend after login/register to link the device's anonymous
  // member to the newly authenticated user account.
  app.patch(
    '/club/me',
    {
      schema: {
        tags: ['Club'],
        summary: 'Link current anonymous Club member to the authenticated user account',
        response: { 200: { type: 'object' }, 401: errorSchema, 404: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await deps.authenticate(request); // requires session
      try {
        const result = await clubs.linkMemberToUser({
          userId: user.id,
          deviceToken: deviceTokenFromRequest(request),
        });
        const config = await clubs.getPublicConfig();
        return reply.send({
          member: serializeMember(result.member),
          linked: result.linked,
          config: serializeSettings(config),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  // ── Merge two Club memberships ───────────────────────────────────────────────
  app.post(
    '/club/merge',
    {
      schema: {
        tags: ['Club'],
        summary: 'Merge an anonymous Club member into the authenticated user\'s member',
        body: {
          type: 'object',
          required: ['anonymousMemberId'],
          properties: {
            anonymousMemberId: { type: 'string', format: 'uuid' },
          },
        },
        response: { 200: { type: 'object' }, 400: errorSchema, 401: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await deps.authenticate(request); // requires session
      const body = request.body as { anonymousMemberId?: string };
      if (!body?.anonymousMemberId) {
        throw new AppError(400, 'CLUB_INVALID_MERGE', 'anonymousMemberId es obligatorio');
      }
      try {
        // The user's registered member is the primary
        const userMember = await clubs.getMemberOrThrow({
          userId: user.id,
          deviceToken: null,
        });
        const result = await clubs.mergeMembers(userMember.id, body.anonymousMemberId);
        const config = await clubs.getPublicConfig();
        return reply.send({
          member: serializeMember(result.primary),
          mergedId: result.merged.id,
          config: serializeSettings(config),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  // ── Recovery codes ───────────────────────────────────────────────────────────
  // Generate new recovery codes (plaintext codes returned — show once only)
  app.post(
    '/club/recovery-codes/generate',
    {
      schema: {
        tags: ['Club'],
        summary: 'Generate new recovery codes for current member',
        body: {
          type: 'object',
          properties: { count: { type: 'integer', minimum: 1, maximum: 10, default: 3 } },
        },
        response: { 200: { type: 'object' }, 401: errorSchema, 403: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await authenticateOptional(deps.authenticate, request);
      const body = request.body as { count?: number } | null;
      const count = body?.count ?? 3;
      try {
        const result = await clubs.generateRecoveryCodes(
          {
            userId: user?.role === 'customer' ? user.id : null,
            deviceToken: deviceTokenFromRequest(request),
          },
          count,
        );
        const config = await clubs.getPublicConfig();
        return reply.send({
          member: serializeMember(result.member),
          codes: result.codes, // plaintext — show once
          config: serializeSettings(config),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  // List active (unused, not expired) recovery codes
  app.get(
    '/club/recovery-codes',
    {
      schema: {
        tags: ['Club'],
        summary: 'List active recovery codes for current member (fingerprints only)',
        response: { 200: { type: 'object' }, 401: errorSchema, 403: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await authenticateOptional(deps.authenticate, request);
      try {
        const result = await clubs.listRecoveryCodes({
          userId: user?.role === 'customer' ? user.id : null,
          deviceToken: deviceTokenFromRequest(request),
        });
        return reply.send({
          member: serializeMember(result.member),
          codes: result.codes.map((code: ClubRecoveryCode) => ({
            id: code.id,
            fingerprint: code.codeFingerprint,
            expiresAt: code.expiresAt.toISOString(),
            createdAt: code.createdAt.toISOString(),
          })),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  // ── Recovery: use a code to link a new device ───────────────────────────────
  app.post(
    '/club/recover',
    {
      schema: {
        tags: ['Club'],
        summary: 'Recover Club membership using a recovery code and a new device token',
        body: {
          type: 'object',
          required: ['code', 'newDeviceToken'],
          properties: {
            code: { type: 'string', minLength: 1 },
            newDeviceToken: { type: 'string', minLength: 1 },
          },
        },
        response: { 200: { type: 'object' }, 400: errorSchema, 403: errorSchema, 404: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const body = request.body as { code?: string; newDeviceToken?: string };
      if (!body?.code || !body?.newDeviceToken) {
        throw new AppError(400, 'CLUB_INVALID_RECOVER', 'code y newDeviceToken son obligatorios');
      }
      try {
        const result = await clubs.recoverByCode(body.code, body.newDeviceToken);
        setClubDeviceCookie(reply, result.deviceToken, isSecureRequest(request));
        const config = await clubs.getPublicConfig();
        return reply.send({
          member: serializeMember(result.member),
          deviceToken: result.deviceToken,
          config: serializeSettings(config),
        });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  // ── POS / public resolve by member code ───────────────────────────────────
  app.get(
    '/club/resolve',
    {
      schema: {
        tags: ['Club'],
        summary: 'Resolve Club member by member code (for TPV)',
        querystring: {
          type: 'object',
          required: ['memberCode'],
          properties: { memberCode: { type: 'string', minLength: 1 } },
        },
        response: { 200: { type: 'object' }, 404: errorSchema, 409: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const query = request.query as { memberCode?: string };
      const code = query.memberCode;
      if (!code || typeof code !== 'string' || !code.trim()) {
        throw new AppError(400, 'CLUB_INVALID_CODE', 'memberCode es obligatorio');
      }
      try {
        const member = await clubs.resolveByCode(code.trim());
        if (!member) throw new ClubMemberNotFoundError();
        const config = await clubs.getPublicConfig();
        return reply.send({ member: serializeMember(member), config: serializeSettings(config) });
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );

  app.get(
    '/admin/club/settings',
    {
      schema: {
        tags: ['Club Admin'],
        summary: 'Get Club settings (admin)',
        response: { 200: { type: 'object' }, 401: errorSchema, 403: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      return reply.send(serializeSettings(await clubs.getAdminSettings()));
    },
  );

  app.patch(
    '/admin/club/settings',
    {
      schema: {
        tags: ['Club Admin'],
        summary: 'Update Club settings (admin)',
        body: { type: 'object' },
        response: { 200: { type: 'object' }, 401: errorSchema, 403: errorSchema },
      } as FastifySchema,
    },
    async (request, reply) => {
      const user = await deps.authenticate(request);
      requireRole(user, 'admin');
      const input = parseJson(adminSettingsSchema, request.body ?? {});
      try {
        const updatedBy = await resolveUpdatedBy(deps.pool, user);
        const settings = await clubs.updateSettings(
          {
            enabled: input.clubEnabled,
            cashbackBps:
              input.cashbackPercentage === undefined
                ? undefined
                : Math.round(input.cashbackPercentage * 100),
            allowAnonymousMembers: input.allowAnonymousMembers,
            allowRecoveryCodes: input.allowRecoveryCodes,
            minimumRedeemAmountCents: input.minimumRedeemAmountCents,
          },
          updatedBy,
        );
        return reply.send(serializeSettings(settings));
      } catch (error) {
        throw mapClubError(error);
      }
    },
  );
}

async function authenticateOptional(
  authenticate: Authenticate,
  request: FastifyRequest,
): Promise<CurrentUser | null> {
  try {
    return await authenticate(request);
  } catch (error) {
    if (error instanceof AppError && error.statusCode === 401) return null;
    throw error;
  }
}

function deviceTokenFromRequest(request: FastifyRequest): string | null {
  const header = request.headers['x-club-device-token'];
  const token = Array.isArray(header) ? header[0] : header;
  if (typeof token === 'string' && token.trim()) return token.trim();
  const cookieToken = request.cookies?.[CLUB_DEVICE_COOKIE_NAME];
  return typeof cookieToken === 'string' && cookieToken.trim() ? cookieToken.trim() : null;
}

function isSecureRequest(request: FastifyRequest): boolean {
  const forwardedProto = request.headers['x-forwarded-proto'];
  const proto = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
  return request.protocol === 'https' || proto === 'https';
}

function setClubDeviceCookie(reply: FastifyReply, token: string, secure: boolean): void {
  void reply.setCookie(CLUB_DEVICE_COOKIE_NAME, token, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure,
    maxAge: CLUB_DEVICE_COOKIE_MAX_AGE_S,
  });
}

async function resolveUpdatedBy(pool: pg.Pool, user: CurrentUser): Promise<string | null> {
  const identityUser = await pool.query<{ id: string }>(`SELECT id FROM identity_users WHERE id = $1`, [
    user.id,
  ]);
  return identityUser.rowCount ? user.id : null;
}

function mapClubError(error: unknown): Error {
  if (error instanceof ClubDisabledError) {
    return new AppError(409, 'CLUB_DISABLED', error.message);
  }
  if (error instanceof ClubAnonymousJoinDisabledError) {
    return new AppError(403, 'CLUB_ANONYMOUS_DISABLED', error.message);
  }
  if (error instanceof ClubDeviceTokenRequiredError) {
    return new AppError(401, 'CLUB_DEVICE_REQUIRED', error.message);
  }
  if (error instanceof ClubMemberNotFoundError) {
    return new AppError(404, 'CLUB_MEMBER_NOT_FOUND', error.message);
  }
  if (error instanceof ClubInsufficientBalanceError) {
    return new AppError(409, 'CLUB_INSUFFICIENT_BALANCE', error.message);
  }
  if (error instanceof InvalidClubTransactionError) {
    return new AppError(400, 'CLUB_INVALID_TRANSACTION', error.message);
  }
  if (error instanceof ClubRecoveryCodeInvalidError) {
    return new AppError(400, 'CLUB_RECOVERY_CODE_INVALID', error.message);
  }
  if (error instanceof ClubRecoveryCodesDisabledError) {
    return new AppError(403, 'CLUB_RECOVERY_CODES_DISABLED', error.message);
  }
  return error instanceof Error ? error : new Error('Unknown club error');
}

function serializeSettings(settings: ClubSettings) {
  return {
    clubEnabled: settings.enabled,
    cashbackBps: settings.cashbackBps,
    cashbackPercentage: settings.cashbackBps / 100,
    allowAnonymousMembers: settings.allowAnonymousMembers,
    allowRecoveryCodes: settings.allowRecoveryCodes,
    minimumRedeemAmountCents: settings.minimumRedeemAmountCents,
  };
}

function serializeMember(member: ClubMember) {
  return {
    id: member.id,
    userId: member.userId,
    memberCode: member.memberCode,
    status: member.status,
    tierCode: member.tierCode,
    currentBalanceCents: member.currentBalanceCents,
    isAnonymous: member.userId === null,
    createdAt: member.createdAt.toISOString(),
    updatedAt: member.updatedAt.toISOString(),
  };
}

function serializeTransaction(transaction: ClubTransaction) {
  return {
    id: transaction.id,
    memberId: transaction.memberId,
    saleId: transaction.saleId,
    storeId: transaction.storeId,
    type: transaction.type,
    amountCents: transaction.amountCents,
    balanceDeltaCents: transaction.balanceDeltaCents,
    idempotencyKey: transaction.idempotencyKey,
    metadata: transaction.metadata,
    createdAt: transaction.createdAt.toISOString(),
  };
}
