const db = require('../models');
const { Op } = require('sequelize');

/**
 * Get current fiscal year for a company by date (default: today).
 * @param {Number} companyId
 * @param {Date} [onDate]
 * @param {Object} [transaction]
 * @returns {Promise<Model|null>} FiscalYear or null
 */
async function getCurrentFiscalYear(companyId, onDate = new Date(), transaction = null) {
  const date = onDate instanceof Date ? onDate : new Date(onDate);
  const dateStr = date.toISOString().slice(0, 10);

  const fy = await db.FiscalYear.findOne({
    where: {
      company_id: companyId,
      start_date: { [Op.lte]: dateStr },
      end_date: { [Op.gte]: dateStr },
      status: { [Op.in]: ['ACTIVE', 'INACTIVE'] }
    },
    transaction
  });
  return fy;
}

/**
 * Generate next transaction number from Number Format Master.
 * Uses company code (Company.code) as short form, resets sequence per fiscal year.
 *
 * @param {Number} companyId - Company ID
 * @param {String} moduleName - Module: PRODUCTION_ORDER, MATERIAL_REQUEST, MATERIAL_ISSUE, PURCHASE_ORDER, PURCHASE_REQUISITION, GOODS_RECEIPT_NOTE, VENDOR_INVOICE, SALES_INVOICE, etc.
 * @param {Number} [fiscalYearId] - Fiscal year ID (optional; resolved from company + today if not provided)
 * @param {Object} [transaction] - Sequelize transaction (optional; creates one if not provided)
 * @returns {Promise<String>} Generated number e.g. 'RI-PO-2025-26-00001'
 */
async function generateNextNumber(companyId, moduleName, fiscalYearId = null, transaction = null) {
  // Backward-compatible parameter handling.
  // Some callers use: generateNextNumber(companyId, moduleName, transaction, fiscalYearId)
  // while others use: generateNextNumber(companyId, moduleName, fiscalYearId, transaction)
  let fyIdArg = fiscalYearId;
  let txnArg = transaction;

  // Case: (companyId, moduleName, txnObject)
  if (fyIdArg && typeof fyIdArg === 'object' && !Array.isArray(fyIdArg)) {
    const looksLikeTxn =
      typeof fyIdArg.commit === 'function' ||
      typeof fyIdArg.rollback === 'function' ||
      typeof fyIdArg.LOCK === 'object';
    if (looksLikeTxn) {
      txnArg = fyIdArg;
      fyIdArg = (typeof txnArg === 'number' ? txnArg : null);
    }
  }

  // Case: (companyId, moduleName, null, fiscalYearIdNumber)
  if ((fyIdArg == null || fyIdArg === '') && typeof txnArg === 'number') {
    fyIdArg = txnArg;
    txnArg = null;
  }

  // Case: (companyId, moduleName, txnObject, fiscalYearIdNumber)
  if (
    fyIdArg &&
    typeof fyIdArg === 'object' &&
    !Array.isArray(fyIdArg) &&
    typeof transaction === 'number'
  ) {
    txnArg = fyIdArg;
    fyIdArg = transaction;
  }

  const txn = txnArg || await db.sequelize.transaction();
  const shouldCommit = !transaction;

  try {
    const company = await db.Company.findByPk(companyId, { attributes: ['id', 'code', 'name'], transaction: txn });
    if (!company) throw new Error(`Company not found: ${companyId}`);
    const companyCode = (company.code || '').trim().toUpperCase() || 'CO';

    let fyId = fyIdArg;
    if (!fyId) {
      const fy = await getCurrentFiscalYear(companyId, new Date(), txn);
      if (!fy) throw new Error(`No active fiscal year found for company ${companyId}. Please pass fiscal_year_id or ensure company has an open fiscal year.`);
      fyId = fy.id;
    } else {
      // If caller passed an FY id from UI, but it doesn't match "today",
      // force the current FY so numbering reflects current financial year.
      const today = new Date();
      const yyyy = today.getFullYear();
      const mm = String(today.getMonth() + 1).padStart(2, '0');
      const dd = String(today.getDate()).padStart(2, '0');
      const todayStr = `${yyyy}-${mm}-${dd}`;

      const passedFy = await db.FiscalYear.findByPk(fyId, { attributes: ['id', 'start_date', 'end_date'], transaction: txn });
      if (passedFy?.start_date && passedFy?.end_date) {
        const inRange = String(passedFy.start_date) <= todayStr && String(passedFy.end_date) >= todayStr;
        if (!inRange) {
          const currentFy = await getCurrentFiscalYear(companyId, today, txn);
          if (currentFy) fyId = currentFy.id;
        }
      }
    }

    const normalizedModuleName = String(moduleName || '').trim().toUpperCase().replace(/\s+/g, '_');
    const spaceModuleName = normalizedModuleName.replace(/_/g, ' ');
    const titleCaseModuleName = spaceModuleName
      .toLowerCase()
      .replace(/\b\w/g, (ch) => ch.toUpperCase());
    const moduleCandidates = Array.from(
      new Set([
        String(moduleName || '').trim(),
        normalizedModuleName,
        spaceModuleName,
        titleCaseModuleName
      ].filter(Boolean))
    );
    const sequenceModuleKey = normalizedModuleName;

    const numberFormat = await db.NumberFormat.findOne({
      where: {
        company_id: companyId,
        module_name: { [Op.in]: moduleCandidates },
        material_type_id: { [Op.or]: [null, 0] },
        is_active: true,
        status: 'ACTIVE'
      },
      // Prefer latest maintained format when multiple active rows exist.
      order: [['updated_at', 'DESC'], ['id', 'DESC']],
      lock: true,
      transaction: txn
    });

    if (!numberFormat) {
      throw new Error(`Number format not found for module: ${moduleName}, company: ${companyId}. Run number format seed or create format in master.`);
    }

    const tokenPrefix = (numberFormat.prefix || numberFormat.format_prefix || moduleName.replace(/_/g, '').slice(0, 6)).toUpperCase();
    const sep = (numberFormat.separator || '-').trim() || '-';
    const seqLen = Math.max(1, parseInt(numberFormat.sequence_length, 10) || 6);
    const includeFY = numberFormat.include_fiscal_year !== false;
    const fyFormat = (numberFormat.fiscal_year_format || 'YYYY-YY').toUpperCase();
    const formatPattern = (numberFormat.format_pattern || '').trim();

    // Ensure YEARLY reset really buckets by *current* financial year.
    // This makes the sequence restart from 1 when FY changes.
    if (String(numberFormat.reset_period || '').toUpperCase() === 'YEARLY') {
      const now = new Date();
      const fyStartMonth = 4; // Apr
      const fyStartDay = 1;
      const month = now.getMonth() + 1;
      const day = now.getDate();
      const startYear = (month > fyStartMonth || (month === fyStartMonth && day >= fyStartDay))
        ? now.getFullYear()
        : now.getFullYear() - 1;

      const expectedStartDate = `${startYear}-04-01`;
      const fyByStart = await db.FiscalYear.findOne({
        where: {
          company_id: companyId,
          start_date: expectedStartDate,
          status: { [Op.in]: ['ACTIVE', 'INACTIVE'] }
        },
        order: [['id', 'DESC']],
        transaction: txn
      });
      if (fyByStart?.id) {
        fyId = fyByStart.id;
      }
    }

    await db.NumberSequence.findOrCreate({
      where: { company_id: companyId, module_name: sequenceModuleKey, fiscal_year_id: fyId },
      defaults: { company_id: companyId, module_name: sequenceModuleKey, fiscal_year_id: fyId, current_sequence: 0 },
      transaction: txn
    });

    const seqRow = await db.NumberSequence.findOne({
      where: { company_id: companyId, module_name: sequenceModuleKey, fiscal_year_id: fyId },
      lock: txn.LOCK.UPDATE,
      transaction: txn
    });
    if (!seqRow) throw new Error('NumberSequence row missing after findOrCreate');

    // Reset sequence when reset_period is YEARLY and FY changed (based on Apr 1 FY).
    if (String(numberFormat.reset_period || '').toUpperCase() === 'YEARLY') {
      const now = new Date();
      const fyStartMonth = 4; // Apr
      const fyStartDay = 1;
      const month = now.getMonth() + 1;
      const day = now.getDate();
      const currentFyStartYear = (month > fyStartMonth || (month === fyStartMonth && day >= fyStartDay))
        ? now.getFullYear()
        : now.getFullYear() - 1;

      const lastReset = numberFormat.last_reset_date ? new Date(numberFormat.last_reset_date) : null;
      let lastResetFyStartYear = null;
      if (lastReset && !Number.isNaN(lastReset.getTime())) {
        const lm = lastReset.getMonth() + 1;
        const ld = lastReset.getDate();
        lastResetFyStartYear = (lm > fyStartMonth || (lm === fyStartMonth && ld >= fyStartDay))
          ? lastReset.getFullYear()
          : lastReset.getFullYear() - 1;
      }

      if (lastResetFyStartYear !== currentFyStartYear) {
        seqRow.current_sequence = 0;
        await seqRow.save({ transaction: txn });

        const yyyy = now.getFullYear();
        const mm = String(now.getMonth() + 1).padStart(2, '0');
        const dd = String(now.getDate()).padStart(2, '0');
        numberFormat.last_reset_date = `${yyyy}-${mm}-${dd}`;
        await numberFormat.save({ transaction: txn });
      }
    }

    seqRow.current_sequence = (seqRow.current_sequence || 0) + 1;
    await seqRow.save({ transaction: txn });

    let fiscalYearStr = '';
    let fiscalYearShort = '';
    let fiscalYearShortRange = '';
    if (includeFY) {
      // Financial year should be based on "today" (FY starts Apr 1 by default),
      // not purely on a FiscalYear row which might be outdated/misconfigured.
      const now = new Date();
      const fyStartMonth = 4; // 1=Jan, 4=Apr (India FY)
      const fyStartDay = 1;

      const month = now.getMonth() + 1;
      const day = now.getDate();
      const startYear = (month > fyStartMonth || (month === fyStartMonth && day >= fyStartDay))
        ? now.getFullYear()
        : now.getFullYear() - 1;

      fiscalYearShort = String(startYear).slice(-2);
      const endShort = String(startYear + 1).slice(-2);
      fiscalYearShortRange = `${fiscalYearShort}-${endShort}`;
      fiscalYearStr = (fyFormat === 'YYYY') ? String(startYear) : `${startYear}-${endShort}`;
    }

    const seqStr = String(seqRow.current_sequence).padStart(seqLen, '0');
    let generatedNumber = '';

    if (formatPattern) {
      generatedNumber = formatPattern
        .replace(/{COMPANY_CODE}/g, companyCode)
        .replace(/{PREFIX}/g, tokenPrefix)
        .replace(/{FY}/g, fiscalYearStr)
        .replace(/{FIN_YEAR}/g, fiscalYearStr)
        .replace(/{FIN_YEAR_SHORT}/g, fiscalYearShortRange)
        .replace(/{FIN_YEAR_START_SHORT}/g, fiscalYearShort)
        .replace(/{SEQUENCE}/g, seqStr)
        .replace(/{SEPARATOR}/g, sep);

      if (numberFormat.suffix) {
        generatedNumber = `${generatedNumber}${sep}${numberFormat.suffix}`;
      }
    } else {
      // Backward-compatible fallback for legacy rows without format_pattern.
      const parts = [companyCode, tokenPrefix];
      if (fiscalYearStr) parts.push(fiscalYearStr);
      parts.push(seqStr);
      generatedNumber = parts.join(sep);
    }

    if (shouldCommit) await txn.commit();
    return generatedNumber;
  } catch (err) {
    if (shouldCommit) await txn.rollback();
    throw err;
  }
}

/**
 * Get number format configuration for a company and module.
 */
async function getNumberFormat(companyId, moduleName, materialTypeId = null) {
  const where = {
    company_id: companyId,
    module_name: moduleName,
    is_active: true,
    status: 'ACTIVE'
  };
  if (materialTypeId != null) where.material_type_id = materialTypeId;
  else where.material_type_id = { [Op.or]: [null, 0] };

  return db.NumberFormat.findOne({ where });
}

module.exports = {
  generateNextNumber,
  getNumberFormat,
  getCurrentFiscalYear
};
