import { MONTH_KEYS, resolveLangBundle } from "./langBundle"

/**
 * Post-processing for `processes/c_invoice_generate`: stamps
 * `C_Invoice.period_of_performance` (the billed month only — "August 2027" / "März 2027",
 * month name in the partner's language) on the invoices a generation run just created — and ONLY on those, taken from
 * the process logs — when the invoiced order is one of the generated fulfilment fee-invoice
 * orders (`C_Order.isFulfillmentOrder = N`, the bookkeeping orders `fulfillment/generate-orders`
 * creates). `Y` is the DB default and marks orders LogYou physically fulfils — their shipment
 * invoices must keep an empty period of performance.
 *
 * Deliberately NO `$expand`: after the process has finished, each generated invoice is handled
 * with plain separate requests — GET invoice → GET its order (gate + period date) → GET the
 * partner (AD_Language) → PUT invoice.
 *
 * Month/year come from the order's `PeriodPerformanceDate` (the fulfillment order generator
 * sets it to the last day of the billed month, so every fulfilment order carries it). Without
 * that date nothing is written — a wrong month on a tax-relevant field is worse than an empty one.
 *
 * MUST NOT THROW: the caller's token-refresh retry would re-run the whole generation
 * process and create the invoices a second time. Every failure is collected in `errors`.
 */

export type PeriodOfPerformanceResult = {
  updated: Array<{ invoiceId: number, documentNo: string, text: string }>
  skipped: Array<{ invoiceId?: number, documentNo?: string, reason: string }>
  errors: Array<{ invoiceId?: number, documentNo?: string, error: string }>
}

const MAX_LENGTH = 150 // ad_column FieldLength of C_Invoice.period_of_performance

/** GET with one retry — the prod REST layer occasionally drops a connection. */
const getWithRetry = async (event: any, token: string, url: string, attempts = 2): Promise<any> => {
  let lastErr: any
  for (let i = 0; i < attempts; i++) {
    try {
      return await event.context.fetch(url, 'GET', token, null)
    } catch (err: any) {
      lastErr = err
      const status = Number(err?.status ?? err?.statusCode ?? err?.response?.status ?? 0)
      if (status === 404) break // a wrong id is not transient
    }
  }
  throw lastErr
}

/** DocumentNo from a process log line — "Invoice Processed: 26100379", "Rechnung …: 26100379" or a bare number. */
const docNoFromLog = (log: any): string => {
  const s = String(log?.msg ?? log ?? '').trim()
  if (/^\d+$/.test(s)) return s
  const m = s.match(/:\s*(\d+)\s*$/)
  return m ? m[1] : ''
}

/** Record id carried by the log entry (P_ID / Record_ID), 0 when absent. */
const recordIdFromLog = (log: any): number => {
  const cand = Number(log?.recordId ?? log?.Record_ID ?? log?.id ?? 0)
  return Number.isFinite(cand) && cand > 0 ? cand : 0
}

const parseYearMonth = (val: any): { year: number, month: number } | null => {
  const m = String(val ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/)
  if (!m) return null
  const year = Number(m[1])
  const month = Number(m[2])
  return month >= 1 && month <= 12 ? { year, month } : null
}

/** "August 2027" — just the performance month, no label; the month name follows the partner language. */
export const buildPeriodOfPerformanceText = (t: Record<string, string>, year: number, month: number): string =>
  `${t[MONTH_KEYS[month - 1]] ?? month} ${year}`.substring(0, MAX_LENGTH)

const fetchInvoice = async (event: any, token: string, log: any): Promise<any> => {
  const docNo = docNoFromLog(log)
  const recordId = recordIdFromLog(log)

  if (recordId) {
    try {
      const inv: any = await getWithRetry(event, token, `models/c_invoice/${recordId}`)
      if (inv?.id && (!docNo || String(inv.DocumentNo) === docNo)) return inv
    } catch {
      // Log id is not (this) invoice's id — fall through to the DocumentNo lookup.
    }
  }

  if (docNo) {
    const filter = encodeURIComponent(`DocumentNo eq '${docNo}' and IsSOTrx eq true`)
    const orderby = encodeURIComponent('Created desc')
    const res: any = await getWithRetry(
      event, token, `models/c_invoice?$filter=${filter}&$orderby=${orderby}&$top=1`
    )
    return res?.records?.[0] ?? null
  }

  return null
}

const errorText = (err: any): string =>
  err?.data?.detail ?? err?.data?.title ?? err?.data?.message ?? err?.message ?? String(err)

export const applyPeriodOfPerformance = async (
  event: any,
  token: string,
  logs: any[],
  sessionLang: string
): Promise<PeriodOfPerformanceResult> => {
  const result: PeriodOfPerformanceResult = { updated: [], skipped: [], errors: [] }
  const seen = new Set<number>()
  const seenLogKeys = new Set<string>()

  for (const log of Array.isArray(logs) ? logs : []) {
    // A repeated log line for the same document must not cost another round of requests.
    const logKey = `${recordIdFromLog(log)}|${docNoFromLog(log)}`
    if (logKey !== '0|' && seenLogKeys.has(logKey)) continue
    seenLogKeys.add(logKey)

    let inv: any = null
    try {
      inv = await fetchInvoice(event, token, log)
      if (!inv?.id) {
        result.skipped.push({ documentNo: docNoFromLog(log) || undefined, reason: 'invoice-not-found' })
        continue
      }
      if (seen.has(inv.id)) continue
      seen.add(inv.id)

      const documentNo = String(inv.DocumentNo ?? '')
      const orderId = inv.C_Order_ID?.id
      if (!orderId) {
        result.skipped.push({ invoiceId: inv.id, documentNo, reason: 'no-order' })
        continue
      }

      // Separate request: the order carries the gate and the period date.
      const order: any = await getWithRetry(event, token, `models/c_order/${orderId}`)

      // Gate: only the generator's bookkeeping orders (isFulfillmentOrder = N). Every order
      // LogYou physically fulfils carries the DB default Y and is skipped. An absent value is
      // treated as Y as well — never stamp a tax-relevant field on an unknown order.
      const isFulfillmentOrder = order?.isFulfillmentOrder ?? order?.IsFulfillmentOrder
      if (isFulfillmentOrder !== false && isFulfillmentOrder !== 'N') {
        result.skipped.push({ invoiceId: inv.id, documentNo, reason: 'not-fulfillment-invoice-order' })
        continue
      }

      const period = parseYearMonth(order?.PeriodPerformanceDate)
      if (!period) {
        result.skipped.push({ invoiceId: inv.id, documentNo, reason: 'no-period-date' })
        continue
      }

      // Separate request: the partner's AD_Language decides the text (de_DE → German,
      // en_US → English); the session language is used only when the partner has none set.
      const partnerId = inv.C_BPartner_ID?.id ?? order?.C_BPartner_ID?.id
      let partnerLanguage: any
      if (partnerId) {
        try {
          const partner: any = await getWithRetry(event, token, `models/c_bpartner/${partnerId}`)
          partnerLanguage = partner?.AD_Language
        } catch {
          // partner unreadable → session language below
        }
      }
      const t = resolveLangBundle(partnerLanguage, sessionLang)
      const text = buildPeriodOfPerformanceText(t, period.year, period.month)

      if (String(inv.period_of_performance ?? '') === text) {
        result.skipped.push({ invoiceId: inv.id, documentNo, reason: 'already-set' })
        continue
      }

      // Column is IsAlwaysUpdateable, so this works on completed (CO) invoices too.
      await event.context.fetch(`models/c_invoice/${inv.id}`, 'PUT', token, { period_of_performance: text })
      result.updated.push({ invoiceId: inv.id, documentNo, text })
    } catch (err: any) {
      result.errors.push({
        invoiceId: inv?.id,
        documentNo: inv?.DocumentNo ? String(inv.DocumentNo) : (docNoFromLog(log) || undefined),
        error: errorText(err)
      })
    }
  }

  return result
}
