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

const TRANSACTION_DIRECTIONS = new Set(['credit', 'debit']);
const TRANSACTION_BOOKING_TYPES = new Set(['manual', 'invoice', 'adjustment', 'payment_claim']);
const TRANSACTION_STATUSES = new Set(['planned', 'booked', 'cancelled']);

const ACCOUNT_TYPES = new Set(['bank', 'cash', 'virtual']);
const ACCOUNT_USAGE_TYPES = new Set(['general', 'membership_fees', 'donations', 'expenses', 'reserve', 'petty_cash']);
const ACCOUNT_STATUSES = new Set(['active', 'inactive', 'archived']);

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

function normalizeIban(value) {
  const normalized = String(value || '').replace(/\s+/g, '').trim().toUpperCase();
  return normalized || null;
}

function normalizeBic(value) {
  const normalized = String(value || '').replace(/\s+/g, '').trim().toUpperCase();
  return normalized || null;
}

function normalizePayload(payload = {}) {
  const accountType = ACCOUNT_TYPES.has(payload.accountType) ? payload.accountType : 'bank';
  const usageType = ACCOUNT_USAGE_TYPES.has(payload.usageType) ? payload.usageType : 'general';
  const status = ACCOUNT_STATUSES.has(payload.status) ? payload.status : 'active';
  return {
    name: trimText(payload.name, 160),
    accountHolder: trimText(payload.accountHolder, 255),
    bankName: trimText(payload.bankName, 160),
    iban: normalizeIban(payload.iban),
    bic: normalizeBic(payload.bic),
    accountType,
    usageType,
    currencyCode: trimText(payload.currencyCode, 3)?.toUpperCase() || 'EUR',
    allowSepaCollections: Boolean(payload.allowSepaCollections),
    allowOutgoingPayments: Boolean(payload.allowOutgoingPayments),
    isDefault: Boolean(payload.isDefault),
    status,
    notes: trimText(payload.notes),
    sortOrder: Number.isFinite(Number(payload.sortOrder)) ? Number(payload.sortOrder) : 0,
  };
}

function normalizeTransactionPayload(payload = {}) {
  return {
    accountId: Number(payload.accountId) || null,
    invoiceId: Number(payload.invoiceId) || null,
    paymentClaimId: Number(payload.paymentClaimId) || null,
    direction: TRANSACTION_DIRECTIONS.has(payload.direction) ? payload.direction : 'credit',
    bookingType: TRANSACTION_BOOKING_TYPES.has(payload.bookingType) ? payload.bookingType : 'manual',
    status: TRANSACTION_STATUSES.has(payload.status) ? payload.status : 'booked',
    bookingDate: trimText(payload.bookingDate, 10),
    valueDate: trimText(payload.valueDate, 10),
    amountCents: Number.parseInt(payload.amountCents, 10) || 0,
    currencyCode: trimText(payload.currencyCode, 3)?.toUpperCase() || 'EUR',
    reference: trimText(payload.reference, 255),
    notes: trimText(payload.notes),
  };
}

function normalizeSearchText(value) {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function extractClaimIdFromReference(reference) {
  const normalized = normalizeSearchText(reference);
  const match = normalized.match(/(?:forderung|beitrag|claim)\s*(?:nr|nummer|no)?\s*(\d+)/);
  if (match) return Number(match[1]) || null;
  const hashMatch = normalized.match(/#\s*(\d+)/);
  if (hashMatch) return Number(hashMatch[1]) || null;
  return null;
}

function transactionMatchesClaimReference(transactionText, claim, member) {
  if (!transactionText) return false;
  const tokens = [
    claim?.id ? `forderung ${claim.id}` : '',
    claim?.id ? `claim ${claim.id}` : '',
    member?.firstName || '',
    member?.lastName || '',
    member?.email || '',
    member?.sepaMandateReference || member?.sepa_mandate_reference || '',
    member?.memberNumber || member?.member_number || '',
    claim?.notes || '',
  ]
    .map(normalizeSearchText)
    .filter(Boolean);
  return tokens.some((token) => token && transactionText.includes(token));
}

function scorePaymentClaimMatch(transaction, claim) {
  const text = normalizeSearchText([transaction.reference, transaction.notes].filter(Boolean).join(' '));
  if (!text) return 0;

  let score = 0;
  const member = claim.member || {};
  const explicitClaimId = extractClaimIdFromReference(transaction.reference);
  if (explicitClaimId && Number(explicitClaimId) === Number(claim.id)) {
    score += 120;
  } else if (text.includes(`forderung ${claim.id}`) || text.includes(`claim ${claim.id}`) || text.includes(`beitrag ${claim.id}`)) {
    score += 90;
  }

  if (transactionMatchesClaimReference(text, claim, member)) {
    score += 40;
  }

  const amountCents = Number(transaction.amountCents || 0);
  const remainingAmountCents = Math.max(0, Number(claim.amountCents || 0) - Number(claim.paidAmountCents || 0));
  if (amountCents === remainingAmountCents) {
    score += 35;
  } else if (amountCents < remainingAmountCents) {
    score += 15;
  } else {
    score -= 20;
  }

  const bookingDate = transaction.bookingDate ? new Date(transaction.bookingDate) : null;
  const dueDate = claim.dueOn ? new Date(claim.dueOn) : null;
  if (bookingDate && dueDate && !Number.isNaN(bookingDate.getTime()) && !Number.isNaN(dueDate.getTime())) {
    const dayDistance = Math.abs(Math.floor((bookingDate.getTime() - dueDate.getTime()) / 86400000));
    if (dayDistance <= 7) score += 20;
    else if (dayDistance <= 30) score += 10;
  }

  return score;
}

async function findBestPaymentClaimMatch(clubId, transaction, dbTransaction = null) {
  if (!(await hasClubPaymentClaimPaidAmountCentsColumn())) {
    return null;
  }

  if (!transaction || transaction.direction !== 'credit' || transaction.status !== 'booked' || Number(transaction.amountCents || 0) <= 0) {
    return null;
  }
  if (transaction.invoiceId || transaction.paymentClaimId) {
    return null;
  }

  const explicitClaimId = extractClaimIdFromReference(transaction.reference);
  if (explicitClaimId) {
    const explicitClaim = await ClubPaymentClaim.findOne({
      where: {
        id: explicitClaimId,
        clubId,
        status: { [Op.in]: ['open', 'partially_paid'] },
        archivedAt: null,
      },
      include: [{ model: Member, as: 'member', required: false }],
      transaction: dbTransaction || undefined,
      lock: dbTransaction ? Transaction.LOCK.UPDATE : undefined,
    });
    if (explicitClaim && Number(transaction.amountCents || 0) <= Math.max(0, Number(explicitClaim.amountCents || 0) - Number(explicitClaim.paidAmountCents || 0))) {
      return explicitClaim;
    }
  }

  const claims = await ClubPaymentClaim.findAll({
    where: {
      clubId,
      status: { [Op.in]: ['open', 'partially_paid'] },
      archivedAt: null,
      currencyCode: transaction.currencyCode || 'EUR',
    },
    include: [{ model: Member, as: 'member', required: false }],
    transaction: dbTransaction || undefined,
    lock: dbTransaction ? Transaction.LOCK.UPDATE : undefined,
    order: [['dueOn', 'ASC'], ['updatedAt', 'ASC']],
  });

  let bestMatch = null;
  let bestScore = 0;
  let secondScore = 0;
  for (const claim of claims) {
    const score = scorePaymentClaimMatch(transaction, claim);
    if (score > bestScore) {
      secondScore = bestScore;
      bestScore = score;
      bestMatch = claim;
    } else if (score > secondScore) {
      secondScore = score;
    }
  }

  if (!bestMatch) return null;
  if (bestScore < 60) return null;
  if (bestScore < secondScore + 20) return null;
  if (Number(transaction.amountCents || 0) > Math.max(0, Number(bestMatch.amountCents || 0) - Number(bestMatch.paidAmountCents || 0))) {
    return null;
  }
  return bestMatch;
}

function buildTransactionIncludes(includePaymentClaims) {
  return includePaymentClaims
    ? [
      { model: ClubAccount, as: 'account', required: false },
      { model: ClubPaymentClaim, as: 'paymentClaim', required: false, include: [{ model: Member, as: 'member', required: false }] },
    ]
    : [
      { model: ClubAccount, as: 'account', required: false },
    ];
}

function validatePayload(payload) {
  if (!payload.name) {
    const error = new Error('Kontobezeichnung ist erforderlich.');
    error.status = 400;
    throw error;
  }

  if (payload.allowSepaCollections && payload.accountType !== 'bank') {
    const error = new Error('SEPA-Einzüge sind nur für Bankkonten möglich.');
    error.status = 400;
    throw error;
  }

  if (payload.allowSepaCollections && !payload.iban) {
    const error = new Error('Für SEPA-Einzüge muss eine IBAN hinterlegt sein.');
    error.status = 400;
    throw error;
  }

  if (payload.status === 'archived' && payload.isDefault) {
    const error = new Error('Ein archiviertes Konto kann nicht das Standardkonto sein.');
    error.status = 400;
    throw error;
  }
}

function validateTransactionPayload(payload) {
  if (!payload.accountId) {
    const error = new Error('Für eine Kontenbewegung muss ein Konto gewählt werden.');
    error.status = 400;
    throw error;
  }

  if (!payload.bookingDate) {
    const error = new Error('Buchungsdatum 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 ensureSingleDefault(clubId, accountId, transaction) {
  await ClubAccount.update(
    { isDefault: false },
    {
      where: {
        clubId,
        id: { [Op.ne]: accountId },
      },
      transaction,
    }
  );
}

async function ensureFallbackDefault(clubId, transaction) {
  const existingDefault = await ClubAccount.findOne({
    where: {
      clubId,
      isDefault: true,
      status: { [Op.ne]: 'archived' },
    },
    transaction,
  });
  if (existingDefault) {
    return;
  }

  const fallback = await ClubAccount.findOne({
    where: {
      clubId,
      status: { [Op.ne]: 'archived' },
    },
    order: [['sortOrder', 'ASC'], ['createdAt', 'ASC']],
    transaction,
  });

  if (fallback) {
    await fallback.update({ isDefault: true }, { transaction });
  }
}

class ClubAccountService {
  async listClubAccounts(clubId) {
    const includePaymentClaims = await hasClubPaymentClaimPaidAmountCentsColumn();
    const [accounts, transactions] = await Promise.all([
      ClubAccount.findAll({
        where: { clubId },
        order: [
          ['isDefault', 'DESC'],
          ['status', 'ASC'],
          ['sortOrder', 'ASC'],
          ['name', 'ASC'],
        ],
      }),
      ClubAccountTransaction.findAll({
        where: { clubId },
        include: buildTransactionIncludes(includePaymentClaims),
        order: [['bookingDate', 'DESC'], ['createdAt', 'DESC']],
        limit: 250,
      }),
    ]);

    return {
      accounts,
      transactions,
    };
  }

  async createTransaction(clubId, userId, payload) {
    const includePaymentClaims = await hasClubPaymentClaimPaidAmountCentsColumn();
    const normalized = normalizeTransactionPayload(payload);
    if (!includePaymentClaims) {
      normalized.paymentClaimId = null;
    }
    validateTransactionPayload(normalized);

    const account = await ClubAccount.findOne({
      where: { id: normalized.accountId, clubId },
    });
    if (!account) {
      const error = new Error('Konto wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    return sequelize.transaction(async (dbTransaction) => {
      let matchedClaim = null;
      if (includePaymentClaims && normalized.paymentClaimId) {
        matchedClaim = await ClubPaymentClaim.findOne({
          where: {
            id: normalized.paymentClaimId,
            clubId,
            status: { [Op.in]: ['open', 'partially_paid'] },
            archivedAt: null,
          },
          include: [{ model: Member, as: 'member', required: false }],
          transaction: dbTransaction,
          lock: Transaction.LOCK.UPDATE,
        });
        if (!matchedClaim) {
          const error = new Error('Zahlungsforderung wurde nicht gefunden.');
          error.status = 404;
          throw error;
        }
      } else if (includePaymentClaims) {
        matchedClaim = await findBestPaymentClaimMatch(clubId, normalized, dbTransaction);
      }

      const transactionPayload = {
        clubId,
        createdByUserId: userId || null,
        ...normalized,
        paymentClaimId: includePaymentClaims ? (matchedClaim ? matchedClaim.id : normalized.paymentClaimId) : null,
        bookingType: matchedClaim ? 'payment_claim' : normalized.bookingType,
      };

      const transaction = await ClubAccountTransaction.create(transactionPayload, { transaction: dbTransaction });

      if (includePaymentClaims && matchedClaim) {
        await clubPaymentClaimService.applyPaymentToClaim(
          clubId,
          matchedClaim,
          {
            amountCents: Number(transaction.amountCents || 0),
          },
          dbTransaction
        );
        await clubPaymentClaimService.reconcileFromTransactions(clubId, matchedClaim.id, dbTransaction);
      }

      return transaction.reload({
        include: buildTransactionIncludes(includePaymentClaims),
        transaction: dbTransaction,
      });
    });
  }

  async updateTransaction(clubId, transactionId, payload) {
    const includePaymentClaims = await hasClubPaymentClaimPaidAmountCentsColumn();
    const transactionRow = await ClubAccountTransaction.findOne({
      where: { id: transactionId, clubId },
    });
    if (!transactionRow) {
      const error = new Error('Kontenbewegung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }
    if (transactionRow.bookingType !== 'manual') {
      const error = new Error('Automatisch erzeugte Kontenbewegungen können nicht manuell bearbeitet werden.');
      error.status = 400;
      throw error;
    }

    const normalized = normalizeTransactionPayload(payload);
    if (!includePaymentClaims) {
      normalized.paymentClaimId = null;
    }
    validateTransactionPayload(normalized);
    const previousPaymentClaimId = Number(transactionRow.paymentClaimId || 0) || null;

    const account = await ClubAccount.findOne({
      where: { id: normalized.accountId, clubId },
    });
    if (!account) {
      const error = new Error('Konto wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    await transactionRow.update(normalized);

    if (includePaymentClaims) {
      const claimIdsToReconcile = new Set([previousPaymentClaimId, normalized.paymentClaimId || null].filter(Boolean));
      for (const claimId of claimIdsToReconcile) {
        await clubPaymentClaimService.reconcileFromTransactions(clubId, claimId);
      }
    }

    return transactionRow.reload({
      include: buildTransactionIncludes(includePaymentClaims),
    });
  }

  async deleteTransaction(clubId, transactionId) {
    const includePaymentClaims = await hasClubPaymentClaimPaidAmountCentsColumn();
    const transactionRow = await ClubAccountTransaction.findOne({
      where: { id: transactionId, clubId },
    });
    if (!transactionRow) {
      const error = new Error('Kontenbewegung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }
    if (transactionRow.bookingType !== 'manual') {
      const error = new Error('Automatisch erzeugte Kontenbewegungen können nicht gelöscht werden.');
      error.status = 400;
      throw error;
    }

    const claimId = Number(transactionRow.paymentClaimId || 0) || null;
    await transactionRow.destroy();
    if (includePaymentClaims && claimId) {
      await clubPaymentClaimService.reconcileFromTransactions(clubId, claimId);
    }
    return { success: true };
  }

  async listAccountTransactions(clubId, accountId = null) {
    const includePaymentClaims = await hasClubPaymentClaimPaidAmountCentsColumn();
    const where = { clubId };
    if (accountId) {
      where.accountId = accountId;
    }

    return ClubAccountTransaction.findAll({
      where,
      include: buildTransactionIncludes(includePaymentClaims),
      order: [
        ['bookingDate', 'DESC'],
        ['createdAt', 'DESC'],
      ],
    });
  }

  async createClubAccount(clubId, payload) {
    const normalized = normalizePayload(payload);
    validatePayload(normalized);

    return sequelize.transaction(async (transaction) => {
      const account = await ClubAccount.create({
        clubId,
        ...normalized,
        archivedAt: normalized.status === 'archived' ? new Date() : null,
      }, { transaction });

      if (normalized.isDefault) {
        await ensureSingleDefault(clubId, account.id, transaction);
      } else {
        await ensureFallbackDefault(clubId, transaction);
      }

      return account.reload({ transaction });
    });
  }

  async updateClubAccount(clubId, accountId, payload) {
    const account = await ClubAccount.findOne({
      where: { id: accountId, clubId },
    });

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

    const normalized = normalizePayload(payload);
    validatePayload(normalized);

    return sequelize.transaction(async (transaction) => {
      await account.update({
        ...normalized,
        archivedAt: normalized.status === 'archived'
          ? (account.archivedAt || new Date())
          : null,
      }, { transaction });

      if (normalized.isDefault) {
        await ensureSingleDefault(clubId, account.id, transaction);
      }

      await ensureFallbackDefault(clubId, transaction);
      return account.reload({ transaction });
    });
  }

  async updateClubAccountStatus(clubId, accountId, status) {
    if (!ACCOUNT_STATUSES.has(status)) {
      const error = new Error('Ungültiger Kontostatus.');
      error.status = 400;
      throw error;
    }

    const account = await ClubAccount.findOne({
      where: { id: accountId, clubId },
    });

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

    return sequelize.transaction(async (transaction) => {
      const updatePayload = {
        status,
        archivedAt: status === 'archived' ? (account.archivedAt || new Date()) : null,
      };
      if (status === 'archived') {
        updatePayload.isDefault = false;
      }

      await account.update(updatePayload, { transaction });
      await ensureFallbackDefault(clubId, transaction);
      return account.reload({ transaction });
    });
  }

  async deleteClubAccount(clubId, accountId) {
    const account = await ClubAccount.findOne({
      where: { id: accountId, clubId },
    });

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

    await sequelize.transaction(async (transaction) => {
      await account.destroy({ transaction });
      await ensureFallbackDefault(clubId, transaction);
    });
  }
}

export default new ClubAccountService();
