import { buildSigningHeaders } from "./ebaySignature"

/**
 * Shared eBay Finances API helper.
 *
 * Auth model: OAuth2 user tokens stored per order source on `c_ordersource`
 * (marketplace_key = client/app id, marketplace_secret = client secret,
 * marketplace_token = JSON blob { access_token, refresh_token, expires_in,
 * obtained_at, ... }, ebay_environment = 'production' | 'sandbox').
 *
 * The OAuth authorization + initial token exchange already live in
 * server/api/oauth/ebay/* (authorize / callback / test-connection). This util
 * adds the read side for the accounting / payout-reconciliation feature:
 * refreshing a stale access token in-memory, then calling the Finances API
 * (Payouts + Transactions) and decomposing transactions into accounting buckets.
 *
 * READ-ONLY: nothing here writes to iDempiere. The access-token refresh is an
 * in-memory eBay call only (refresh tokens do not rotate, so it is side-effect
 * free); the refreshed token is intentionally NOT persisted.
 */

export const EBAY_TOKEN_URL = {
  production: 'https://api.ebay.com/identity/v1/oauth2/token',
  sandbox: 'https://api.sandbox.ebay.com/identity/v1/oauth2/token'
}

// The Finances API is served from the apiz.* host.
export const EBAY_API_BASE = {
  production: 'https://apiz.ebay.com',
  sandbox: 'https://apiz.sandbox.ebay.com'
}

// Scope requested on refresh — must be a subset of the originally granted scopes
// (sell.finances is already granted by the authorize flow).
const EBAY_REFRESH_SCOPE = 'https://api.ebay.com/oauth/api_scope/sell.finances'

export type EbayCategory =
  | 'umsaetze'
  | 'retouren'
  | 'ebayGebuehren'
  | 'versandkosten'
  | 'werbekosten'
  | 'auszahlung'
  | 'sonstiges'

// Parse an amount that may arrive as a number, a string, or an eBay
// { value, currency } money object. Tolerant of en ("1,234.56") and de
// ("1.234,56") formatting, though eBay normally uses dot decimals.
export const parseAmount = (v: any): number => {
  if (v == null) return 0
  if (typeof v === 'object') return parseAmount(v.value)
  let s = String(v).trim()
  if (!s) return 0
  const hasComma = s.includes(',')
  const hasDot = s.includes('.')
  if (hasComma && hasDot) {
    if (s.lastIndexOf(',') > s.lastIndexOf('.')) s = s.replace(/\./g, '').replace(',', '.')
    else s = s.replace(/,/g, '')
  } else if (hasComma) {
    s = s.replace(',', '.')
  }
  const n = parseFloat(s)
  return isNaN(n) ? 0 : n
}

// --- Token handling -------------------------------------------------------

// Parse the marketplace_token field — handles an object, a JSON string, a
// double-encoded JSON string, or a JSON blob truncated by the DB column length
// (falls back to a regex extraction of access/refresh tokens).
export const parseEbayTokenBlob = (raw: any): any | null => {
  if (!raw) return null
  if (typeof raw === 'object') return raw
  if (typeof raw === 'string') {
    try {
      let t = JSON.parse(raw)
      if (typeof t === 'string') t = JSON.parse(t)
      return t
    } catch {
      const accessMatch = raw.match(/"access_token"\s*:\s*"([^"]+)"/)
      const refreshMatch = raw.match(/"refresh_token"\s*:\s*"([^"]+)"/)
      if (accessMatch || refreshMatch) {
        return { access_token: accessMatch?.[1], refresh_token: refreshMatch?.[1] }
      }
      return null
    }
  }
  return null
}

// Is the stored access token still valid (with a 2-minute safety buffer)?
const isAccessTokenFresh = (tokens: any): boolean => {
  if (!tokens?.access_token) return false
  if (!tokens.obtained_at || !tokens.expires_in) return false
  const obtained = new Date(tokens.obtained_at).getTime()
  if (isNaN(obtained)) return false
  const expiresMs = obtained + Number(tokens.expires_in) * 1000
  return Date.now() < expiresMs - 120000
}

// Exchange a refresh token for a fresh access token (in-memory, not persisted).
export const refreshEbayAccessToken = async (
  clientId: string,
  clientSecret: string,
  refreshToken: string,
  environment: string = 'production'
): Promise<{ access_token: string; expires_in: number }> => {
  const url = environment === 'sandbox' ? EBAY_TOKEN_URL.sandbox : EBAY_TOKEN_URL.production
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${credentials}`
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      scope: EBAY_REFRESH_SCOPE
    }).toString()
  })

  const text = await response.text()
  let json: any = {}
  try { json = text ? JSON.parse(text) : {} } catch { json = { raw: text } }

  if (!response.ok || !json.access_token) {
    const msg = json?.error_description || json?.error || text || response.statusText
    const err: any = new Error(`eBay token refresh failed (${response.status}): ${msg}`)
    err.status = response.status
    err.ebay = json
    throw err
  }
  return { access_token: json.access_token, expires_in: json.expires_in }
}

// Everything needed to make a signed Finances API call: the per-user access
// token, the environment, and the app client id/secret (needed to mint the
// Ed25519 signing key for eBay's mandatory digital signatures).
export interface EbayContext {
  accessToken: string
  environment: string
  clientId: string
  clientSecret: string
}

// Resolve a full call context from an order source, refreshing the access token
// if stale. Throws Error carrying a .status so callers can surface it verbatim.
export const getEbayContext = async (orderSource: any): Promise<EbayContext> => {
  const clientId = orderSource?.SHOPWARE_KEY || orderSource?.marketplace_key
  const clientSecret = orderSource?.SHOPWARE_SECRET || orderSource?.marketplace_secret
  const environment = orderSource?.ebay_environment || 'production'

  const tokens = parseEbayTokenBlob(orderSource?.marketplace_token)
  if (!tokens) {
    const err: any = new Error('eBay account is not connected (no stored token). Connect it under Order Source settings.')
    err.status = 400
    throw err
  }

  let accessToken: string
  if (isAccessTokenFresh(tokens)) {
    accessToken = tokens.access_token
  } else if (tokens.refresh_token && clientId && clientSecret) {
    const refreshed = await refreshEbayAccessToken(clientId, clientSecret, tokens.refresh_token, environment)
    accessToken = refreshed.access_token
  } else if (tokens.access_token) {
    // Can't refresh — try the stored access token anyway; a 401 surfaces clearly.
    accessToken = tokens.access_token
  } else {
    const err: any = new Error('eBay token expired and no refresh token available. Please reconnect the eBay account.')
    err.status = 401
    throw err
  }

  return { accessToken, environment, clientId, clientSecret }
}

// --- Finances API ---------------------------------------------------------

// Generic SIGNED eBay GET. eBay's Finances API mandates RFC 9421 digital
// signatures (else 403 "Missing x-ebay-signature-key header"), so every request
// carries the x-ebay-signature-key / Signature / Signature-Input headers built
// from the app's Ed25519 signing key. Returns parsed JSON, throws an Error
// carrying the eBay error message + status on a non-2xx.
export const ebayApiGet = async (ctx: EbayContext, path: string): Promise<any> => {
  const base = ctx.environment === 'sandbox' ? EBAY_API_BASE.sandbox : EBAY_API_BASE.production
  const url = `${base}${path}`

  const signHeaders = await buildSigningHeaders({
    method: 'GET',
    url,
    clientId: ctx.clientId,
    clientSecret: ctx.clientSecret,
    environment: ctx.environment
  })

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${ctx.accessToken}`,
      'Content-Type': 'application/json',
      ...signHeaders
    }
  })

  const text = await response.text()
  let json: any = {}
  try { json = text ? JSON.parse(text) : {} } catch { json = { raw: text } }

  if (!response.ok) {
    const msg = json?.errors?.[0]?.message || json?.errors?.[0]?.longMessage ||
      json?.error_description || text || response.statusText
    const err: any = new Error(`eBay API ${response.status}: ${msg}`)
    err.status = response.status
    err.ebay = json
    throw err
  }
  return json
}

// List payouts (the lump-sum bank deposits = "settlements"). Most recent first.
export const listPayouts = async (
  ctx: EbayContext,
  opts: { payoutDateFrom?: string; payoutDateTo?: string; limit?: number; offset?: number } = {}
): Promise<{ payouts: any[]; total: number; limit: number; offset: number }> => {
  const limit = opts.limit ?? 200
  const offset = opts.offset ?? 0
  let path = `/sell/finances/v1/payout?limit=${limit}&offset=${offset}`
  if (opts.payoutDateFrom || opts.payoutDateTo) {
    const from = opts.payoutDateFrom || ''
    const to = opts.payoutDateTo || ''
    path += `&filter=${encodeURIComponent(`payoutDate:[${from}..${to}]`)}`
  }
  const json = await ebayApiGet(ctx, path)
  return {
    payouts: json.payouts || [],
    total: json.total ?? (json.payouts?.length || 0),
    limit: json.limit ?? limit,
    offset: json.offset ?? offset
  }
}

// Fetch a single payout (for its authoritative amount + date).
export const getPayout = async (ctx: EbayContext, payoutId: string): Promise<any> => {
  return await ebayApiGet(ctx, `/sell/finances/v1/payout/${encodeURIComponent(payoutId)}`)
}

// Fetch ALL transactions belonging to a payout (paginated; eBay caps limit at 1000).
export const getTransactionsForPayout = async (
  ctx: EbayContext,
  payoutId: string,
  maxRows: number = 20000
): Promise<any[]> => {
  const all: any[] = []
  const limit = 1000
  let offset = 0
  let total = Number.POSITIVE_INFINITY

  while (offset < total && all.length < maxRows) {
    const filter = `payoutId:{${payoutId}}`
    const path = `/sell/finances/v1/transaction?limit=${limit}&offset=${offset}&filter=${encodeURIComponent(filter)}`
    const json = await ebayApiGet(ctx, path)
    const txns = json.transactions || []
    all.push(...txns)
    total = json.total ?? all.length
    if (!txns.length) break
    offset += limit
  }
  return all
}

// --- Categorisation -------------------------------------------------------

const lc = (v: any) => String(v ?? '').trim().toLowerCase()

// Promoted Listings / advertising fee?
const isAdvertisingFee = (feeType: string, extra: string = ''): boolean => {
  const s = lc(`${feeType} ${extra}`)
  return s.includes('ad_fee') || s.includes('ad fee') || s.includes('advertis') || s.includes('promoted')
}

export interface EbaySettlementRow {
  category: EbayCategory
  transactionType: string
  bookingEntry: string
  amountDescription: string
  amount: number
  orderId: string
  sku: string
  qty: number | null
  postedDate: string
}

// Explode one eBay transaction into one or more signed accounting rows.
// eBay embeds selling fees INSIDE the SALE transaction (orderLineItems[].marketplaceFees
// + totalFeeAmount) rather than as separate transactions, so a SALE is split into a
// positive Umsätze row plus negative fee rows. Signs follow bookingEntry
// (CREDIT = +, DEBIT = -) so that the sum of all rows reconciles to the payout total.
export const decomposeTransaction = (txn: any): EbaySettlementRow[] => {
  const rows: EbaySettlementRow[] = []
  const type = String(txn?.transactionType || '').toUpperCase()
  const booking = String(txn?.bookingEntry || '').toUpperCase()
  const value = parseAmount(txn?.amount)
  const sign = booking === 'DEBIT' ? -1 : 1
  const orderId = txn?.orderId || ''
  const date = txn?.transactionDate || ''
  const memo = txn?.transactionMemo || ''

  // Collect embedded selling fees across all order line items.
  const fees: { feeType: string; amount: number; sku: string }[] = []
  for (const oli of (txn?.orderLineItems || [])) {
    for (const f of (oli?.marketplaceFees || [])) {
      fees.push({
        feeType: f?.feeType || 'FEE',
        amount: parseAmount(f?.amount),
        sku: oli?.legacyItemId || oli?.lineItemId || ''
      })
    }
  }

  if (type === 'SALE') {
    rows.push({
      category: 'umsaetze',
      transactionType: 'SALE',
      bookingEntry: booking || 'CREDIT',
      amountDescription: memo || 'Verkauf',
      amount: value,
      orderId,
      sku: '',
      qty: null,
      postedDate: date
    })

    if (fees.length) {
      for (const f of fees) {
        rows.push({
          category: isAdvertisingFee(f.feeType) ? 'werbekosten' : 'ebayGebuehren',
          transactionType: 'FEE',
          bookingEntry: 'DEBIT',
          amountDescription: f.feeType,
          amount: -Math.abs(f.amount),
          orderId,
          sku: f.sku || '',
          qty: null,
          postedDate: date
        })
      }
    } else {
      const totalFee = parseAmount(txn?.totalFeeAmount)
      if (totalFee) {
        rows.push({
          category: 'ebayGebuehren',
          transactionType: 'FEE',
          bookingEntry: 'DEBIT',
          amountDescription: 'Verkaufsgebühren',
          amount: -Math.abs(totalFee),
          orderId,
          sku: '',
          qty: null,
          postedDate: date
        })
      }
    }
    return rows
  }

  if (type === 'REFUND' || type === 'DISPUTE') {
    rows.push({
      category: 'retouren',
      transactionType: type,
      bookingEntry: booking || 'DEBIT',
      amountDescription: memo || (type === 'DISPUTE' ? 'Zahlungsstreit' : 'Erstattung'),
      amount: sign * value,
      orderId,
      sku: '',
      qty: null,
      postedDate: date
    })
    return rows
  }

  if (type === 'SHIPPING_LABEL') {
    rows.push({
      category: 'versandkosten',
      transactionType: 'SHIPPING_LABEL',
      bookingEntry: booking || 'DEBIT',
      amountDescription: memo || 'Versandetikett',
      amount: sign * value,
      orderId,
      sku: '',
      qty: null,
      postedDate: date
    })
    return rows
  }

  if (type === 'NON_SALE_CHARGE') {
    const feeType = txn?.feeType || ''
    rows.push({
      category: isAdvertisingFee(feeType, memo) ? 'werbekosten' : 'ebayGebuehren',
      transactionType: 'NON_SALE_CHARGE',
      bookingEntry: booking || 'DEBIT',
      amountDescription: feeType || memo || 'Gebühr',
      amount: sign * value,
      orderId,
      sku: '',
      qty: null,
      postedDate: date
    })
    return rows
  }

  // CREDIT, TRANSFER, ADJUSTMENT, WITHDRAWAL, LOAN_REPAYMENT, unknown → sonstiges
  // (surfaced, never silently dropped — mirrors the Amazon settlement catch-all).
  rows.push({
    category: 'sonstiges',
    transactionType: type || 'UNKNOWN',
    bookingEntry: booking || '',
    amountDescription: txn?.feeType || memo || type || 'Sonstiges',
    amount: sign * value,
    orderId,
    sku: '',
    qty: null,
    postedDate: date
  })
  return rows
}
