import sequelize from '../database.js';
import Club from '../models/Club.js';
import ClubAccountTransaction from '../models/ClubAccountTransaction.js';
import ClubInvoice from '../models/ClubInvoice.js';
import ClubInvoiceItem from '../models/ClubInvoiceItem.js';
import ClubInvoiceParty from '../models/ClubInvoiceParty.js';
import ClubAccount from '../models/ClubAccount.js';

const INVOICE_DIRECTIONS = new Set(['incoming', 'outgoing']);
const INVOICE_STATUSES = new Set(['draft', 'issued', 'partially_paid', 'paid', 'cancelled', 'archived']);
const PARTY_TYPES = new Set(['customer', 'supplier', 'sponsor', 'other']);
const INVOICE_TYPES = new Set(['membership_fee', 'course_fee', 'sponsoring', 'material', 'service', 'expense', 'other']);

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

function normalizeDate(value) {
  const normalized = String(value || '').trim();
  return normalized || null;
}

function parseNumber(value, fallback = 0) {
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : fallback;
}

function roundToInt(value) {
  return Math.round(Number(value) || 0);
}

function normalizePartyPayload(payload = {}) {
  return {
    name: trimText(payload.name, 255),
    partyType: PARTY_TYPES.has(payload.partyType) ? payload.partyType : 'customer',
    status: trimText(payload.status, 32) || 'active',
    contractReference: trimText(payload.contractReference, 120),
    validFrom: normalizeDate(payload.validFrom),
    validTo: normalizeDate(payload.validTo),
    contactName: trimText(payload.contactName, 255),
    email: trimText(payload.email, 255),
    phone: trimText(payload.phone, 80),
    street: trimText(payload.street, 255),
    postalCode: trimText(payload.postalCode, 24),
    city: trimText(payload.city, 120),
    countryCode: trimText(payload.countryCode, 2)?.toUpperCase() || 'DE',
    iban: trimText(payload.iban, 34)?.replace(/\s+/g, '').toUpperCase() || null,
    bic: trimText(payload.bic, 11)?.replace(/\s+/g, '').toUpperCase() || null,
    taxIdentifier: trimText(payload.taxIdentifier, 64),
    notes: trimText(payload.notes),
  };
}

function normalizeInvoiceItems(items = []) {
  return (Array.isArray(items) ? items : [])
    .map((item, index) => {
      const quantity = parseNumber(item.quantity, 1);
      const unitPriceCents = roundToInt(item.unitPriceCents);
      const taxRate = parseNumber(item.taxRate, 0);
      const netLineCents = roundToInt(quantity * unitPriceCents);
      const taxLineCents = roundToInt(netLineCents * (taxRate / 100));
      const totalCents = netLineCents + taxLineCents;
      return {
        lineNo: index + 1,
        description: trimText(item.description) || '',
        quantity,
        unitPriceCents,
        taxRate,
        netLineCents,
        taxLineCents,
        totalCents,
      };
    })
    .filter((item) => item.description);
}

function normalizeInvoicePayload(payload = {}) {
  return {
    invoiceDirection: INVOICE_DIRECTIONS.has(payload.invoiceDirection) ? payload.invoiceDirection : 'outgoing',
    invoiceType: INVOICE_TYPES.has(payload.invoiceType) ? payload.invoiceType : 'other',
    status: INVOICE_STATUSES.has(payload.status) ? payload.status : 'draft',
    externalReference: trimText(payload.externalReference, 255),
    partyId: payload.partyId ? Number(payload.partyId) : null,
    accountId: payload.accountId ? Number(payload.accountId) : null,
    issuedOn: normalizeDate(payload.issuedOn),
    dueOn: normalizeDate(payload.dueOn),
    paidOn: normalizeDate(payload.paidOn),
    currencyCode: trimText(payload.currencyCode, 3)?.toUpperCase() || 'EUR',
    description: trimText(payload.description),
    items: normalizeInvoiceItems(payload.items),
  };
}

function validateInvoicePayload(payload) {
  if (!payload.partyId) {
    const error = new Error('Bitte eine Rechnungspartei auswählen.');
    error.status = 400;
    throw error;
  }

  if (payload.items.length === 0) {
    const error = new Error('Mindestens eine Rechnungsposition ist erforderlich.');
    error.status = 400;
    throw error;
  }
}

function summarizeItems(items) {
  return items.reduce((acc, item) => {
    acc.netAmountCents += item.netLineCents;
    acc.taxAmountCents += item.taxLineCents;
    acc.grossAmountCents += item.totalCents;
    return acc;
  }, {
    netAmountCents: 0,
    taxAmountCents: 0,
    grossAmountCents: 0,
  });
}

function buildInvoiceNumber(prefix, nextNumber, referenceDate = new Date()) {
  const parsedReferenceDate = referenceDate instanceof Date ? referenceDate : new Date(referenceDate);
  const year = Number.isNaN(parsedReferenceDate.getTime()) ? new Date().getFullYear() : parsedReferenceDate.getFullYear();
  const normalizedPrefix = String(prefix || '').trim().toUpperCase();
  const paddedCounter = String(Math.max(1, Number.parseInt(nextNumber, 10) || 1)).padStart(4, '0');
  return normalizedPrefix ? `${normalizedPrefix}-${year}-${paddedCounter}` : `${year}-${paddedCounter}`;
}

function deriveInvoiceTransactionData(invoice) {
  if (!invoice?.accountId || invoice.status === 'draft') {
    return null;
  }

  const amountCents = Math.max(0, Number.parseInt(invoice.grossAmountCents, 10) || 0);
  if (!amountCents) {
    return null;
  }

  const bookingDate = invoice.paidOn || invoice.issuedOn || invoice.dueOn || new Date().toISOString().slice(0, 10);
  const status = invoice.status === 'paid'
    ? 'booked'
    : ['issued', 'partially_paid'].includes(invoice.status)
      ? 'planned'
      : 'cancelled';

  return {
    clubId: invoice.clubId,
    accountId: invoice.accountId,
    invoiceId: invoice.id,
    direction: invoice.invoiceDirection === 'incoming' ? 'debit' : 'credit',
    bookingType: 'invoice',
    status,
    bookingDate,
    valueDate: bookingDate,
    amountCents,
    currencyCode: invoice.currencyCode || 'EUR',
    reference: invoice.invoiceNumber || invoice.externalReference || `Rechnung ${invoice.id}`,
    notes: invoice.description || null,
  };
}

async function syncInvoiceAccountTransaction(invoice, transaction) {
  const transactionData = deriveInvoiceTransactionData(invoice);
  const existingTransaction = await ClubAccountTransaction.findOne({
    where: {
      clubId: invoice.clubId,
      invoiceId: invoice.id,
      bookingType: 'invoice',
    },
    transaction,
  });

  if (!transactionData) {
    if (existingTransaction) {
      await existingTransaction.destroy({ transaction });
    }
    return null;
  }

  if (existingTransaction) {
    await existingTransaction.update(transactionData, { transaction });
    return existingTransaction;
  }

  return ClubAccountTransaction.create(transactionData, { transaction });
}

async function generateNextInvoiceNumber(clubId, invoiceDirection, issuedOn, transaction) {
  const club = await Club.findByPk(clubId, {
    transaction,
    lock: transaction.LOCK.UPDATE,
  });

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

  const isIncoming = invoiceDirection === 'incoming';
  const prefixField = isIncoming ? 'incomingInvoicePrefix' : 'outgoingInvoicePrefix';
  const nextNumberField = isIncoming ? 'incomingInvoiceNextNumber' : 'outgoingInvoiceNextNumber';
  const referenceDate = issuedOn || new Date();
  let nextNumber = Math.max(1, Number.parseInt(club[nextNumberField], 10) || 1);
  let invoiceNumber = buildInvoiceNumber(club[prefixField], nextNumber, referenceDate);

  // Falls der Nummernkreis manuell zurückgesetzt wurde, wird bis zur nächsten freien Nummer weitergezählt.
  while (await ClubInvoice.count({
    where: { clubId, invoiceNumber },
    transaction,
    lock: transaction.LOCK.UPDATE,
  })) {
    nextNumber += 1;
    invoiceNumber = buildInvoiceNumber(club[prefixField], nextNumber, referenceDate);
  }

  await club.update({
    [nextNumberField]: nextNumber + 1,
  }, { transaction });

  return invoiceNumber;
}

class ClubInvoiceService {
  async listClubInvoices(clubId) {
    const [club, parties, accounts, invoices] = await Promise.all([
      Club.findByPk(clubId, {
        attributes: [
          'id',
          'outgoingInvoicePrefix',
          'outgoingInvoiceNextNumber',
          'incomingInvoicePrefix',
          'incomingInvoiceNextNumber',
        ],
      }),
      ClubInvoiceParty.findAll({
        where: { clubId },
        order: [['name', 'ASC']],
      }),
      ClubAccount.findAll({
        where: { clubId },
        order: [['isDefault', 'DESC'], ['name', 'ASC']],
      }),
      ClubInvoice.findAll({
        where: { clubId },
        include: [
          { model: ClubInvoiceParty, as: 'party', required: false },
          { model: ClubInvoiceItem, as: 'items', required: false },
          { model: ClubAccount, as: 'account', required: false },
        ],
        order: [['updatedAt', 'DESC'], [{ model: ClubInvoiceItem, as: 'items' }, 'lineNo', 'ASC']],
      }),
    ]);

    return {
      parties,
      accounts,
      invoices,
      settings: club ? {
        outgoingInvoicePrefix: club.outgoingInvoicePrefix || 'RE',
        outgoingInvoiceNextNumber: club.outgoingInvoiceNextNumber || 1,
        incomingInvoicePrefix: club.incomingInvoicePrefix || 'EI',
        incomingInvoiceNextNumber: club.incomingInvoiceNextNumber || 1,
      } : null,
    };
  }

  async createInvoiceParty(clubId, payload) {
    const normalized = normalizePartyPayload(payload);
    if (!normalized.name) {
      const error = new Error('Name der Rechnungspartei ist erforderlich.');
      error.status = 400;
      throw error;
    }
    return ClubInvoiceParty.create({ clubId, ...normalized });
  }

  async updateInvoiceParty(clubId, partyId, payload) {
    const party = await ClubInvoiceParty.findOne({ where: { id: partyId, clubId } });
    if (!party) {
      const error = new Error('Rechnungspartei wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }
    const normalized = normalizePartyPayload(payload);
    if (!normalized.name) {
      const error = new Error('Name der Rechnungspartei ist erforderlich.');
      error.status = 400;
      throw error;
    }
    await party.update(normalized);
    return party;
  }

  async deleteInvoiceParty(clubId, partyId) {
    const party = await ClubInvoiceParty.findOne({ where: { id: partyId, clubId } });
    if (!party) {
      const error = new Error('Rechnungspartei wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }
    const linkedCount = await ClubInvoice.count({ where: { clubId, partyId } });
    if (linkedCount > 0) {
      const error = new Error('Rechnungspartei kann nicht gelöscht werden, weil bereits Rechnungen darauf verweisen.');
      error.status = 400;
      throw error;
    }
    await party.destroy();
  }

  async createInvoice(clubId, userId, payload) {
    const normalized = normalizeInvoicePayload(payload);
    validateInvoicePayload(normalized);
    const totals = summarizeItems(normalized.items);

    return sequelize.transaction(async (transaction) => {
      const invoiceNumber = await generateNextInvoiceNumber(
        clubId,
        normalized.invoiceDirection,
        normalized.issuedOn,
        transaction
      );

      const invoice = await ClubInvoice.create({
        clubId,
        createdByUserId: userId || null,
        ...normalized,
        invoiceNumber,
        ...totals,
        archivedAt: normalized.status === 'archived' ? new Date() : null,
      }, { transaction });

      await ClubInvoiceItem.bulkCreate(
        normalized.items.map((item) => ({
          invoiceId: invoice.id,
          lineNo: item.lineNo,
          description: item.description,
          quantity: item.quantity,
          unitPriceCents: item.unitPriceCents,
          taxRate: item.taxRate,
          totalCents: item.totalCents,
        })),
        { transaction }
      );

      await syncInvoiceAccountTransaction(invoice, transaction);

      return ClubInvoice.findOne({
        where: { id: invoice.id, clubId },
        include: [
          { model: ClubInvoiceParty, as: 'party', required: false },
          { model: ClubInvoiceItem, as: 'items', required: false },
          { model: ClubAccount, as: 'account', required: false },
        ],
        transaction,
      });
    });
  }

  async updateInvoice(clubId, invoiceId, payload) {
    const invoice = await ClubInvoice.findOne({ where: { id: invoiceId, clubId } });
    if (!invoice) {
      const error = new Error('Rechnung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    const normalized = normalizeInvoicePayload(payload);
    validateInvoicePayload(normalized);
    const totals = summarizeItems(normalized.items);

    return sequelize.transaction(async (transaction) => {
      if (invoice.invoiceNumber && normalized.invoiceDirection !== invoice.invoiceDirection) {
        const error = new Error('Die Rechnungsrichtung kann nach Vergabe der Rechnungsnummer nicht mehr geändert werden.');
        error.status = 400;
        throw error;
      }

      const invoiceNumber = invoice.invoiceNumber || await generateNextInvoiceNumber(
        clubId,
        normalized.invoiceDirection,
        normalized.issuedOn || invoice.issuedOn,
        transaction
      );

      await invoice.update({
        ...normalized,
        invoiceNumber,
        ...totals,
        archivedAt: normalized.status === 'archived' ? (invoice.archivedAt || new Date()) : null,
      }, { transaction });

      await ClubInvoiceItem.destroy({
        where: { invoiceId: invoice.id },
        transaction,
      });

      await ClubInvoiceItem.bulkCreate(
        normalized.items.map((item) => ({
          invoiceId: invoice.id,
          lineNo: item.lineNo,
          description: item.description,
          quantity: item.quantity,
          unitPriceCents: item.unitPriceCents,
          taxRate: item.taxRate,
          totalCents: item.totalCents,
        })),
        { transaction }
      );

      await syncInvoiceAccountTransaction(invoice, transaction);

      return ClubInvoice.findOne({
        where: { id: invoice.id, clubId },
        include: [
          { model: ClubInvoiceParty, as: 'party', required: false },
          { model: ClubInvoiceItem, as: 'items', required: false },
          { model: ClubAccount, as: 'account', required: false },
        ],
        transaction,
      });
    });
  }

  async updateInvoiceStatus(clubId, invoiceId, status) {
    if (!INVOICE_STATUSES.has(status)) {
      const error = new Error('Ungültiger Rechnungsstatus.');
      error.status = 400;
      throw error;
    }

    const invoice = await ClubInvoice.findOne({ where: { id: invoiceId, clubId } });
    if (!invoice) {
      const error = new Error('Rechnung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    await sequelize.transaction(async (transaction) => {
      await invoice.update({
        status,
        archivedAt: status === 'archived' ? (invoice.archivedAt || new Date()) : null,
        paidOn: status === 'paid' ? (invoice.paidOn || new Date().toISOString().slice(0, 10)) : invoice.paidOn,
      }, { transaction });
      await syncInvoiceAccountTransaction(invoice, transaction);
    });

    return invoice.reload({
      include: [
        { model: ClubInvoiceParty, as: 'party', required: false },
        { model: ClubInvoiceItem, as: 'items', required: false },
        { model: ClubAccount, as: 'account', required: false },
      ],
    });
  }

  async deleteInvoice(clubId, invoiceId) {
    const invoice = await ClubInvoice.findOne({ where: { id: invoiceId, clubId } });
    if (!invoice) {
      const error = new Error('Rechnung wurde nicht gefunden.');
      error.status = 404;
      throw error;
    }

    await sequelize.transaction(async (transaction) => {
      await ClubAccountTransaction.destroy({
        where: {
          clubId,
          invoiceId,
          bookingType: 'invoice',
        },
        transaction,
      });
      await ClubInvoiceItem.destroy({ where: { invoiceId }, transaction });
      await invoice.destroy({ transaction });
    });
  }
}

export default new ClubInvoiceService();
