import { Op, Transaction } from 'sequelize';
import sequelize from '../database.js';
import { ClubAccount, ClubAccountTransaction, ClubPaymentClaim, Member } from '../models/index.js';
import { hasClubPaymentClaimPaidAmountCentsColumn } from './clubPaymentClaimCompatibility.js';

const CLAIM_TYPES = new Set(['membership_fee', 'additional_fee', 'course_fee', 'penalty_fee', 'other']);
const CLAIM_STATUSES = new Set(['open', 'partially_paid', 'paid', 'written_off', 'cancelled']);

function trimText(value, maxLength = null) {
  const normalized = String(value || '').trim();
  if (!normalized) return null;
  return maxLength ? normalized.slice(0, maxLength) : normalized;
}

function normalizePayload(payload = {}) {
  return {
    memberId: Number(payload.memberId) || null,
    feeRuleId: Number(payload.feeRuleId) || null,
    claimType: CLAIM_TYPES.has(payload.claimType) ? payload.claimType : 'membership_fee',
    status: CLAIM_STATUSES.has(payload.status) ? payload.status : 'open',
    dueOn: trimText(payload.dueOn, 10),
    amountCents: Number.parseInt(payload.amountCents, 10) || 0,
    paidAmountCents: Math.max(0, Number.parseInt(payload.paidAmountCents, 10) || 0),
    currencyCode: trimText(payload.currencyCode, 3)?.toUpperCase() || 'EUR',
    reminderLevel: Math.max(0, Number.parseInt(payload.reminderLevel, 10) || 0),
    notes: trimText(payload.notes),
  };
}

function resolveReminderTimestamp(existingClaim, nextReminderLevel) {
  const previousReminderLevel = Math.max(0, Number.parseInt(existingClaim?.reminderLevel, 10) || 0);
  if (nextReminderLevel <= 0) return null;
  if (nextReminderLevel > previousReminderLevel) return new Date();
  return existingClaim?.lastReminderAt || new Date();
}

function clampPaidAmount(amountCents, paidAmountCents) {
  return Math.max(0, Math.min(Number(amountCents || 0), Number(paidAmountCents || 0)));
}

function deriveFinancialStatus(status, amountCents, paidAmountCents) {
  if (status === 'cancelled' || status === 'written_off') return status;
  if (Number(amountCents || 0) <= 0) return 'open';
  if (paidAmountCents >= amountCents) return 'paid';
  if (paidAmountCents > 0) return 'partially_paid';
  return 'open';
}

function deriveStatusFromBalance(amountCents, paidAmountCents) {
  if (Number(amountCents || 0) <= 0) return 'open';
  if (paidAmountCents >= amountCents) return 'paid';
  if (paidAmountCents > 0) return 'partially_paid';
  return 'open';
}

function validatePayload(payload) {
  if (!payload.memberId) {
    const error = new Error('Mitglied ist erforderlich.');
    error.status = 400;
    throw error;
  }
  if (!payload.dueOn) {
    const error = new Error('Fälligkeitsdatum ist erforderlich.');
    error.status = 400;
    throw error;
  }
  if (!Number.isFinite(Number(payload.amountCents)) || Number(payload.amountCents) <= 0) {
    const error = new Error('Der Betrag muss größer als 0 sein.');
    error.status = 400;
    throw error;
  }
}

async function ensureMemberBelongsToClub(clubId, memberId) {
  const member = await Member.findOne({ where: { id: memberId, clubId } });
  if (!member) {
    const error = new Error('Mitglied wurde nicht gefunden.');
    error.status = 404;
    throw error;
  }
  return member;
}

class ClubPaymentClaimService {
  async listClaims(clubId) {
    if (!(await hasClubPaymentClaimPaidAmountCentsColumn())) {
      return { claims: [] };
    }

    const claims = await ClubPaymentClaim.findAll({
      where: {
        clubId,
        [Op.or]: [
          { archivedAt: null },
          { archivedAt: { [Op.is]: null } },
        ],
        status: { [Op.notIn]: ['written_off', 'cancelled'] },
      },
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
      order: [['dueOn', 'ASC'], ['updatedAt', 'DESC']],
    });

    return { claims };
  }

  async createClaim(clubId, payload) {
    const normalized = normalizePayload(payload);
    validatePayload(normalized);
    await ensureMemberBelongsToClub(clubId, normalized.memberId);
    const paidAmountCents = clampPaidAmount(normalized.amountCents, normalized.paidAmountCents);
    const status = deriveFinancialStatus(normalized.status, normalized.amountCents, paidAmountCents);

    const claim = await ClubPaymentClaim.create({
      clubId,
      ...normalized,
      paidAmountCents,
      status,
      settledAt: status === 'paid' ? new Date() : null,
      lastPaidAt: paidAmountCents > 0 ? new Date() : null,
      lastReminderAt: normalized.reminderLevel > 0 ? new Date() : null,
    });

    return claim.reload({
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
    });
  }

  async updateClaim(clubId, claimId, payload) {
    const claim = await ClubPaymentClaim.findOne({ where: { id: claimId, clubId } });
    if (!claim) {
      const error = new Error('Zahlungsforderung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    const normalized = normalizePayload(payload);
    validatePayload(normalized);
    await ensureMemberBelongsToClub(clubId, normalized.memberId);
    const paidAmountCents = clampPaidAmount(normalized.amountCents, normalized.paidAmountCents);
    const status = deriveFinancialStatus(normalized.status, normalized.amountCents, paidAmountCents);
    const previousPaidAmount = Number(claim.paidAmountCents || 0);

    await claim.update({
      ...normalized,
      paidAmountCents,
      status,
      settledAt: status === 'paid' ? (claim.settledAt || new Date()) : null,
      lastPaidAt: paidAmountCents > previousPaidAmount ? new Date() : (paidAmountCents > 0 ? (claim.lastPaidAt || new Date()) : null),
      lastReminderAt: resolveReminderTimestamp(claim, normalized.reminderLevel),
      archivedAt: ['written_off', 'cancelled'].includes(status) ? (claim.archivedAt || new Date()) : null,
    });

    return claim.reload({
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
    });
  }

  async updateClaimStatus(clubId, claimId, status) {
    if (!CLAIM_STATUSES.has(status)) {
      const error = new Error('Ungültiger Zahlungsstatus.');
      error.status = 400;
      throw error;
    }

    const claim = await ClubPaymentClaim.findOne({ where: { id: claimId, clubId } });
    if (!claim) {
      const error = new Error('Zahlungsforderung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    await claim.update({
      status,
      paidAmountCents: status === 'paid' ? Number(claim.amountCents || 0) : Number(claim.paidAmountCents || 0),
      settledAt: status === 'paid' ? (claim.settledAt || new Date()) : null,
      lastPaidAt: status === 'paid' ? (claim.lastPaidAt || new Date()) : claim.lastPaidAt,
      archivedAt: ['written_off', 'cancelled'].includes(status) ? (claim.archivedAt || new Date()) : null,
    });

    return claim.reload({
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
    });
  }

  async deleteClaim(clubId, claimId) {
    const claim = await ClubPaymentClaim.findOne({ where: { id: claimId, clubId } });
    if (!claim) {
      const error = new Error('Zahlungsforderung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    await claim.destroy();
    return { success: true };
  }

  async registerPayment(clubId, claimId, payload = {}) {
    const amountCents = Number.parseInt(payload.amountCents, 10) || 0;
    if (amountCents <= 0) {
      const error = new Error('Der Zahlungsbetrag muss größer als 0 sein.');
      error.status = 400;
      throw error;
    }

    const claim = await ClubPaymentClaim.findOne({ where: { id: claimId, clubId } });
    if (!claim) {
      const error = new Error('Zahlungsforderung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }
    if (['cancelled', 'written_off', 'paid'].includes(claim.status)) {
      const error = new Error('Für diesen Status kann keine Teilzahlung mehr erfasst werden.');
      error.status = 400;
      throw error;
    }
    const accountId = Number(payload.accountId) || null;
    let account = null;
    if (accountId) {
      account = await ClubAccount.findOne({ where: { id: accountId, clubId } });
      if (!account) {
        const error = new Error('Konto wurde nicht gefunden.');
        error.status = 404;
        throw error;
      }
    }

    const nextPaidAmount = clampPaidAmount(claim.amountCents, Number(claim.paidAmountCents || 0) + amountCents);
    const actualPaymentAmountCents = nextPaidAmount - Number(claim.paidAmountCents || 0);
    if (actualPaymentAmountCents <= 0) {
      const error = new Error('Die Forderung ist bereits vollständig beglichen.');
      error.status = 400;
      throw error;
    }
    const nextStatus = deriveFinancialStatus(claim.status, claim.amountCents, nextPaidAmount);
    const note = trimText(payload.note);
    const notePrefix = `Teilzahlung ${formatMoney(actualPaymentAmountCents, claim.currencyCode)} erfasst`;
    const appendedNotes = notePrefix
      ? [claim.notes, `${notePrefix}${note ? `: ${note}` : ''}`].filter(Boolean).join('\n')
      : claim.notes;
    const bookingDate = trimText(payload.bookingDate, 10) || new Date().toISOString().slice(0, 10);

    await sequelize.transaction(async (transaction) => {
      await this.applyPaymentToClaim(clubId, claim, {
        amountCents: actualPaymentAmountCents,
        paidAmountCents: nextPaidAmount,
        status: nextStatus,
        notes: appendedNotes,
      }, transaction);

      if (account) {
        await ClubAccountTransaction.create({
          clubId,
          accountId: account.id,
          paymentClaimId: claim.id,
          direction: 'credit',
          bookingType: 'payment_claim',
          status: 'booked',
          bookingDate,
          valueDate: bookingDate,
          amountCents: actualPaymentAmountCents,
          currencyCode: claim.currencyCode || account.currencyCode || 'EUR',
          reference: payload.reference ? trimText(payload.reference, 255) : `Forderung #${claim.id}`,
          notes: note || `Zahlung zu Forderung #${claim.id}`,
        }, { transaction });
      }
    });

    return claim.reload({
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
    });
  }

  async reconcileFromTransactions(clubId, claimId, dbTransaction = null) {
    const claim = await ClubPaymentClaim.findOne({
      where: { id: claimId, clubId },
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
      transaction: dbTransaction || undefined,
      lock: dbTransaction ? Transaction.LOCK.UPDATE : undefined,
    });

    if (!claim) {
      const error = new Error('Zahlungsforderung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    if (['cancelled', 'written_off'].includes(claim.status)) {
      return claim;
    }

    const transactions = await ClubAccountTransaction.findAll({
      where: {
        clubId,
        paymentClaimId: claim.id,
        direction: 'credit',
        status: 'booked',
      },
      order: [['bookingDate', 'ASC'], ['createdAt', 'ASC']],
      transaction: dbTransaction || undefined,
      lock: dbTransaction ? Transaction.LOCK.UPDATE : undefined,
    });

    const paidAmountCents = transactions.reduce((sum, transaction) => sum + Number(transaction.amountCents || 0), 0);
    const latestPaymentTransaction = transactions[transactions.length - 1] || null;
    const nextStatus = deriveStatusFromBalance(Number(claim.amountCents || 0), paidAmountCents);

    await claim.update({
      paidAmountCents,
      status: nextStatus,
      settledAt: nextStatus === 'paid' ? (claim.settledAt || new Date()) : null,
      lastPaidAt: paidAmountCents > 0
        ? (latestPaymentTransaction?.bookingDate ? new Date(`${latestPaymentTransaction.bookingDate}T00:00:00`) : (claim.lastPaidAt || new Date()))
        : null,
      archivedAt: nextStatus === 'paid' ? null : claim.archivedAt,
    }, dbTransaction ? { transaction: dbTransaction } : undefined);

    return claim.reload({
      include: [
        { model: Member, as: 'member', required: false },
        { model: ClubAccountTransaction, as: 'transactions', required: false },
      ],
      transaction: dbTransaction || undefined,
    });
  }

  async applyPaymentToClaim(clubId, claimOrId, payment, dbTransaction = null) {
    const claimId = typeof claimOrId === 'object' && claimOrId ? claimOrId.id : claimOrId;
    const claim = typeof claimOrId === 'object' && claimOrId ? claimOrId : await ClubPaymentClaim.findOne({
      where: { id: claimId, clubId },
      transaction: dbTransaction || undefined,
      lock: dbTransaction ? Transaction.LOCK.UPDATE : undefined,
    });

    if (!claim) {
      const error = new Error('Zahlungsforderung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    if (['cancelled', 'written_off', 'paid'].includes(claim.status)) {
      const error = new Error('Für diesen Status kann keine Zahlung mehr erfasst werden.');
      error.status = 400;
      throw error;
    }

    const incomingAmountCents = Number.parseInt(payment?.amountCents, 10) || 0;
    if (incomingAmountCents <= 0) {
      const error = new Error('Der Zahlungsbetrag muss größer als 0 sein.');
      error.status = 400;
      throw error;
    }

    const currentPaidAmount = Number(claim.paidAmountCents || 0);
    const nextPaidAmount = clampPaidAmount(claim.amountCents, payment.paidAmountCents != null
      ? Number(payment.paidAmountCents)
      : currentPaidAmount + incomingAmountCents);
    const appliedAmountCents = nextPaidAmount - currentPaidAmount;
    if (appliedAmountCents <= 0) {
      const error = new Error('Die Forderung ist bereits vollständig beglichen.');
      error.status = 400;
      throw error;
    }

    const nextStatus = payment.status || deriveFinancialStatus(claim.status, claim.amountCents, nextPaidAmount);
    await claim.update({
      paidAmountCents: nextPaidAmount,
      status: nextStatus,
      settledAt: nextStatus === 'paid' ? (claim.settledAt || new Date()) : null,
      lastPaidAt: new Date(),
      notes: payment.notes != null ? payment.notes : claim.notes,
      lastReminderAt: payment.lastReminderAt != null ? payment.lastReminderAt : claim.lastReminderAt,
    }, dbTransaction ? { transaction: dbTransaction } : undefined);

    return {
      claim: await claim.reload({
        include: [
          { model: Member, as: 'member', required: false },
          { model: ClubAccountTransaction, as: 'transactions', required: false },
        ],
        transaction: dbTransaction || undefined,
      }),
      appliedAmountCents,
      nextPaidAmount,
      nextStatus,
    };
  }
}

function formatMoney(amountCents, currencyCode = 'EUR') {
  return new Intl.NumberFormat('de-DE', {
    style: 'currency',
    currency: currencyCode || 'EUR',
  }).format(Number(amountCents || 0) / 100);
}

export default new ClubPaymentClaimService();
