import type {
  ClubMember,
  ClubResolveInput,
  ClubSettings,
  GenerateRecoveryCodesResult,
  JoinClubResult,
  LinkMemberResult,
  MergeMembersResult,
  RecoverByCodeResult,
  RecordClubTransactionCommand,
  RecordClubTransactionResult,
  UpdateClubSettingsCommand,
} from '../domain/club.js';
import {
  ClubAnonymousJoinDisabledError,
  ClubDeviceTokenRequiredError,
  ClubDisabledError,
  ClubMemberNotFoundError,
  ClubRecoveryCodeInvalidError,
  ClubRecoveryCodesDisabledError,
  InvalidClubTransactionError,
} from '../domain/errors.js';
import type { ClubRepository } from '../domain/ports.js';
import { generateDeviceToken, hashDeviceToken } from '../infrastructure/device-token.js';
import { generateMemberCode } from '../infrastructure/member-code.js';

export class ClubService {
  constructor(private readonly clubs: ClubRepository) {}

  getPublicConfig(): Promise<ClubSettings> {
    return this.clubs.getSettings();
  }

  getAdminSettings(): Promise<ClubSettings> {
    return this.clubs.getSettings();
  }

  async updateSettings(
    input: UpdateClubSettingsCommand,
    updatedBy: string | null,
  ): Promise<ClubSettings> {
    if (input.cashbackBps !== undefined && (!Number.isInteger(input.cashbackBps) || input.cashbackBps < 0)) {
      throw new InvalidClubTransactionError('El cashback del Club debe ser un entero no negativo');
    }
    if (
      input.minimumRedeemAmountCents !== undefined &&
      (!Number.isInteger(input.minimumRedeemAmountCents) || input.minimumRedeemAmountCents < 0)
    ) {
      throw new InvalidClubTransactionError(
        'El mínimo de canje del Club debe ser un entero no negativo',
      );
    }
    return this.clubs.updateSettings(input, updatedBy);
  }

  async joinAnonymous(existingDeviceToken?: string | null): Promise<JoinClubResult> {
    const normalizedExisting = normalizeToken(existingDeviceToken);
    if (normalizedExisting) {
      const existing = await this.clubs.findMemberByDeviceTokenHash(
        hashDeviceToken(normalizedExisting),
        true,
      );
      if (existing) {
        return { member: existing, deviceToken: normalizedExisting, created: false };
      }
    }

    const settings = await this.clubs.getSettings();
    if (!settings.enabled) throw new ClubDisabledError();
    if (!settings.allowAnonymousMembers) throw new ClubAnonymousJoinDisabledError();

    const deviceToken = generateDeviceToken();
    const deviceTokenHash = hashDeviceToken(deviceToken);
    for (let attempt = 0; attempt < 10; attempt += 1) {
      const member = await this.clubs.createMemberWithDevice({
        memberCode: generateMemberCode(),
        deviceTokenHash,
      });
      if (member) return { member, deviceToken, created: true };
    }
    throw new Error('No se pudo generar un código único para el Club');
  }

  resolveMember(input: ClubResolveInput): Promise<ClubMember | null> {
    return this.clubs.resolveMember({
      ...input,
      deviceTokenHash: input.deviceToken ? hashDeviceToken(input.deviceToken) : null,
    });
  }

  async resolveByCode(code: string): Promise<ClubMember | null> {
    if (!code || typeof code !== 'string') return null;
    const normalized = code.trim().toUpperCase();
    if (!normalized) return null;
    return this.clubs.findMemberByCode(normalized);
  }

  async getMemberOrThrow(input: ClubResolveInput): Promise<ClubMember> {
    const normalized = {
      userId: input.userId ?? null,
      deviceToken: normalizeToken(input.deviceToken),
    };
    if (!normalized.userId && !normalized.deviceToken) {
      throw new ClubDeviceTokenRequiredError();
    }
    const member = await this.resolveMember(normalized);
    if (!member) throw new ClubMemberNotFoundError();
    return member;
  }

  async listMovements(input: ClubResolveInput, limit = 50) {
    const safeLimit = Math.min(Math.max(limit, 1), 200);
    const member = await this.getMemberOrThrow(input);
    const items = await this.clubs.listTransactions(member.id, safeLimit);
    return { member, items };
  }

  async generateRecoveryCodes(input: ClubResolveInput, count = 3): Promise<GenerateRecoveryCodesResult> {
    const member = await this.getMemberOrThrow(input);
    const settings = await this.clubs.getSettings();
    if (!settings.allowRecoveryCodes) throw new ClubRecoveryCodesDisabledError();

    const safeCount = Math.min(Math.max(count, 1), 10);
    const generated = await this.clubs.generateRecoveryCodes(member.id, safeCount, 30);
    return {
      member,
      codes: generated.map((g) => g.code),
    };
  }

  async listRecoveryCodes(input: ClubResolveInput) {
    const member = await this.getMemberOrThrow(input);
    const settings = await this.clubs.getSettings();
    if (!settings.allowRecoveryCodes) throw new ClubRecoveryCodesDisabledError();

    const codes = await this.clubs.listActiveRecoveryCodes(member.id);
    return { member, codes };
  }

  async linkMemberToUser(input: ClubResolveInput): Promise<LinkMemberResult> {
    if (!input.userId) throw new ClubDeviceTokenRequiredError();
    const member = await this.getMemberOrThrow(input);
    return this.clubs.linkMemberToUser(member.id, input.userId);
  }

  async mergeMembers(primaryMemberId: string, mergedMemberId: string): Promise<MergeMembersResult> {
    if (primaryMemberId === mergedMemberId) {
      throw new InvalidClubTransactionError('No se puede fusionar un socio consigo mismo');
    }
    return this.clubs.mergeMembers(primaryMemberId, mergedMemberId);
  }

  async recoverByCode(code: string, newDeviceToken: string): Promise<RecoverByCodeResult> {
    if (!code || typeof code !== 'string' || !code.trim()) {
      throw new ClubRecoveryCodeInvalidError('Código de recuperación obligatorio');
    }
    if (!newDeviceToken || typeof newDeviceToken !== 'string' || !newDeviceToken.trim()) {
      throw new ClubDeviceTokenRequiredError();
    }

    const settings = await this.clubs.getSettings();
    if (!settings.allowRecoveryCodes) throw new ClubRecoveryCodesDisabledError();

    const normalizedCode = code.trim().toUpperCase();
    const fingerprint = hashDeviceToken(normalizedCode).slice(0, 16);
    const newDeviceTokenHash = hashDeviceToken(newDeviceToken.trim());

    let result: { memberId: string; fingerprint: string };
    try {
      result = await this.clubs.consumeRecoveryCode(fingerprint, newDeviceTokenHash);
    } catch (error) {
      if (error instanceof Error && error.message.includes('no encontrado')) {
        throw new ClubRecoveryCodeInvalidError();
      }
      throw error;
    }

    const member = await this.clubs.findMemberByUserId(result.memberId);
    if (!member) {
      const byDevice = await this.clubs.findMemberByDeviceTokenHash(result.memberId, false);
      if (!byDevice) throw new ClubMemberNotFoundError();
      return { member: byDevice, deviceToken: newDeviceToken.trim(), usedCodeFingerprint: result.fingerprint };
    }
    return { member, deviceToken: newDeviceToken.trim(), usedCodeFingerprint: result.fingerprint };
  }

  async recordTransaction(
    input: RecordClubTransactionCommand,
  ): Promise<RecordClubTransactionResult> {
    if (!Number.isInteger(input.amountCents) || input.amountCents < 0) {
      throw new InvalidClubTransactionError('El importe base del movimiento Club no es válido');
    }
    if (!Number.isInteger(input.balanceDeltaCents) || input.balanceDeltaCents === 0) {
      throw new InvalidClubTransactionError('El delta de saldo Club no es válido');
    }
    return this.clubs.recordTransaction({
      ...input,
      idempotencyKey: input.idempotencyKey ?? null,
      metadata: input.metadata ?? {},
      saleId: input.saleId ?? null,
      storeId: input.storeId ?? null,
    });
  }
}

function normalizeToken(value: string | null | undefined): string | null {
  if (!value) return null;
  const normalized = value.trim();
  return normalized ? normalized : null;
}
