import { getDb } from '../db/client.js'
import { PaymentSessionStatus, PairingFailureCause, UserType, WsMessage, SessionMode } from '../constants.js'
import { recordFailure } from './pairing-failure.service.js'
import { sendToUser } from '../plugins/websocket.js'
import { sendPushNotification } from './notification.service.js'
import { getPaymentSessionTimeoutMinutes as readPaymentSessionTimeoutMinutes } from './config.service.js'

const sql = getDb()

const getPaymentSessionTimeoutMinutes = async () => {
  const { payment_session_timeout_minutes } = await readPaymentSessionTimeoutMinutes()
  return payment_session_timeout_minutes
}

/**
 * Create a new payment session in `pending` status.
 * Reads `payment_session_timeout_minutes` from config to compute expires_at.
 *
 * Phase 4: `isFirstSessionDiscount` replaces the old `isFreeTrial` flag. Voice-call
 * mode is a routing/badge thing — the price comes from the call tier group, not from
 * the mode itself.
 */
export const createPaymentSession = async ({
  customerId,
  durationMinutes,
  amount,
  isFirstSessionDiscount = false,
  isExtension = false,
  targetedMitraId = null,
  mode = SessionMode.CHAT,
}) => {
  if (!customerId) {
    throw Object.assign(new Error('customerId is required'), { code: 'VALIDATION_ERROR', statusCode: 422 })
  }
  if (typeof durationMinutes !== 'number' || durationMinutes <= 0) {
    throw Object.assign(new Error('durationMinutes must be a positive number'), { code: 'VALIDATION_ERROR', statusCode: 422 })
  }
  if (typeof amount !== 'number' || amount < 0) {
    throw Object.assign(new Error('amount must be a non-negative number'), { code: 'VALIDATION_ERROR', statusCode: 422 })
  }
  if (mode !== SessionMode.CHAT && mode !== SessionMode.CALL) {
    throw Object.assign(new Error('mode must be chat or call'), { code: 'VALIDATION_ERROR', statusCode: 422 })
  }

  const ttlMinutes = await getPaymentSessionTimeoutMinutes()

  const [row] = await sql`
    INSERT INTO payment_sessions (
      customer_id, amount, duration_minutes, is_first_session_discount, is_extension,
      status, targeted_mitra_id, mode, expires_at
    )
    VALUES (
      ${customerId}, ${amount}, ${durationMinutes}, ${isFirstSessionDiscount}, ${isExtension},
      ${PaymentSessionStatus.PENDING}, ${targetedMitraId}, ${mode},
      NOW() + (${ttlMinutes} || ' minutes')::interval
    )
    RETURNING id, customer_id, amount, duration_minutes, is_first_session_discount, is_extension,
              mode, status, targeted_mitra_id, created_at, confirmed_at, consumed_at, expires_at
  `

  return row
}

/**
 * Transition pending → confirmed. Throws on ownership/status/expiry mismatch.
 */
export const confirmPaymentSession = async (paymentSessionId, customerId) => {
  const [existing] = await sql`
    SELECT id, customer_id, status, expires_at
    FROM payment_sessions
    WHERE id = ${paymentSessionId}
  `
  if (!existing) {
    throw Object.assign(new Error('Payment session not found'), { code: 'NOT_FOUND', statusCode: 404 })
  }
  if (existing.customer_id !== customerId) {
    throw Object.assign(new Error('Payment session does not belong to this customer'), { code: 'FORBIDDEN', statusCode: 403 })
  }
  if (existing.status !== PaymentSessionStatus.PENDING) {
    throw Object.assign(new Error(`Payment session is ${existing.status}, cannot confirm`), {
      code: 'INVALID_STATE', statusCode: 409,
    })
  }
  if (new Date(existing.expires_at) <= new Date()) {
    // Inline expiry check in addition to the background sweeper, since the customer can
    // attempt to confirm a row that's already past expires_at before the sweep runs.
    await sql`
      UPDATE payment_sessions SET status = ${PaymentSessionStatus.EXPIRED}
      WHERE id = ${paymentSessionId} AND status = ${PaymentSessionStatus.PENDING}
    `
    throw Object.assign(new Error('Payment session has expired'), { code: 'EXPIRED', statusCode: 409 })
  }

  const [updated] = await sql`
    UPDATE payment_sessions
    SET status = ${PaymentSessionStatus.CONFIRMED}, confirmed_at = NOW()
    WHERE id = ${paymentSessionId} AND status = ${PaymentSessionStatus.PENDING}
    RETURNING id, customer_id, amount, duration_minutes, is_first_session_discount, is_extension,
              mode, status, targeted_mitra_id, created_at, confirmed_at, consumed_at, expires_at
  `
  if (!updated) {
    throw Object.assign(new Error('Payment session state changed during confirm'), { code: 'CONFLICT', statusCode: 409 })
  }
  return updated
}

/**
 * Transition confirmed → consumed. Called from pairing service when a chat starts.
 * Idempotent at higher level (caller should check status first if it matters).
 */
export const consumePaymentSession = async (paymentSessionId) => {
  const [updated] = await sql`
    UPDATE payment_sessions
    SET status = ${PaymentSessionStatus.CONSUMED}, consumed_at = NOW()
    WHERE id = ${paymentSessionId} AND status = ${PaymentSessionStatus.CONFIRMED}
    RETURNING id, status, consumed_at
  `
  return updated || null
}

/**
 * TERMINAL: mark a confirmed payment session as failed_pairing AND write a pairing_failures row.
 * Idempotent: no-op if already terminal (consumed/failed_pairing/expired/abandoned).
 *
 * Use only for true terminal failures (no fallback path possible):
 *   - general blast exhausted, no acceptance
 *   - all blasted mitras explicitly rejected
 *   - customer cancels mid-search
 *   - payment session expires before consumption
 *
 * For intermediate failures that have a fallback CTA available (targeted-mitra reject/timeout
 * during a returning-chat flow), use `recordIntermediateFailure` instead — that writes the
 * audit row WITHOUT terminating the payment session. Termination is the caller's decision
 * (cancel CTA = terminal, fallback-to-blast CTA = stays confirmed).
 */
export const failPaymentSession = async (paymentSessionId, causeTag) => {
  if (!Object.values(PairingFailureCause).includes(causeTag)) {
    throw Object.assign(new Error(`Unknown cause_tag: ${causeTag}`), { code: 'VALIDATION_ERROR', statusCode: 422 })
  }

  const [existing] = await sql`
    SELECT id, customer_id, targeted_mitra_id, amount, status
    FROM payment_sessions
    WHERE id = ${paymentSessionId}
  `
  if (!existing) {
    return null
  }
  // Idempotent: only confirmed sessions transition to failed_pairing here.
  // Pending sessions become expired/abandoned via their own paths.
  if (existing.status !== PaymentSessionStatus.CONFIRMED) {
    return existing
  }

  const [updated] = await sql`
    UPDATE payment_sessions
    SET status = ${PaymentSessionStatus.FAILED_PAIRING}
    WHERE id = ${paymentSessionId} AND status = ${PaymentSessionStatus.CONFIRMED}
    RETURNING id, customer_id, targeted_mitra_id, amount, status
  `
  if (!updated) {
    return existing
  }

  await recordFailure({
    paymentSessionId,
    customerId: existing.customer_id,
    targetedMitraId: existing.targeted_mitra_id,
    causeTag,
    amount: existing.amount,
  })

  return updated
}

/**
 * INTERMEDIATE: write a pairing_failures audit row WITHOUT terminating the payment session.
 *
 * Used for failures inside a flow that still has a fallback path: targeted "Curhat lagi"
 * reject/timeout (customer can fall back to general blast on the same payment), or any
 * other intermediate attempt where the payment_session must remain `confirmed` so it can be
 * reused.
 *
 * One payment_session may FK from multiple pairing_failures rows — that's the desired CC
 * UX (each attempt lists as its own row in the Failed Pairings table).
 *
 * Returns the inserted pairing_failures row, or null if the payment session was missing.
 */
export const recordIntermediateFailure = async ({ paymentSessionId, customerId, targetedMitraId = null, causeTag, amount }) => {
  if (!Object.values(PairingFailureCause).includes(causeTag)) {
    throw Object.assign(new Error(`Unknown cause_tag: ${causeTag}`), { code: 'VALIDATION_ERROR', statusCode: 422 })
  }

  // Loose sanity: the row should exist. If not, fall through with null — caller likely
  // already moved on; we'd rather skip the audit than throw mid-callback.
  const [existing] = await sql`SELECT id FROM payment_sessions WHERE id = ${paymentSessionId}`
  if (!existing) return null

  return recordFailure({
    paymentSessionId,
    customerId,
    targetedMitraId,
    causeTag,
    amount,
  })
}

/**
 * Customer-initiated abandonment of a `pending` payment session (e.g. closed payment screen).
 * No pairing_failures row is written — only confirmed-but-no-chat counts as a pairing failure.
 */
export const abandonPaymentSession = async (paymentSessionId, customerId) => {
  const [existing] = await sql`
    SELECT id, customer_id, status FROM payment_sessions WHERE id = ${paymentSessionId}
  `
  if (!existing) {
    throw Object.assign(new Error('Payment session not found'), { code: 'NOT_FOUND', statusCode: 404 })
  }
  if (existing.customer_id !== customerId) {
    throw Object.assign(new Error('Payment session does not belong to this customer'), { code: 'FORBIDDEN', statusCode: 403 })
  }
  if (existing.status !== PaymentSessionStatus.PENDING) {
    // Idempotent — already terminal.
    return existing
  }
  const [updated] = await sql`
    UPDATE payment_sessions SET status = ${PaymentSessionStatus.ABANDONED}
    WHERE id = ${paymentSessionId} AND status = ${PaymentSessionStatus.PENDING}
    RETURNING id, customer_id, status
  `
  return updated || existing
}

/**
 * Background sweeper:
 *   - pending rows past expires_at → expired (no failure row; never confirmed)
 *   - confirmed rows past expires_at AND not consumed → failed_pairing with cause = payment_session_expired
 */
export const expireStalePaymentSessions = async () => {
  // 1) pending → expired
  const expired = await sql`
    UPDATE payment_sessions
    SET status = ${PaymentSessionStatus.EXPIRED}
    WHERE status = ${PaymentSessionStatus.PENDING}
      AND expires_at <= NOW()
    RETURNING id
  `

  // 2) confirmed-but-stale → failed_pairing. Single atomic UPDATE returns the rows we
  // actually flipped (vs. the old SELECT + per-row UPDATE which leaked a TOCTOU window
  // with concurrent confirmPaymentSession/consumePaymentSession). Audit-row writes and
  // customer notifications then fan out in parallel.
  const flipped = await sql`
    UPDATE payment_sessions
    SET status = ${PaymentSessionStatus.FAILED_PAIRING}
    WHERE status = ${PaymentSessionStatus.CONFIRMED}
      AND expires_at <= NOW()
    RETURNING id, customer_id, targeted_mitra_id, amount
  `

  await Promise.all(flipped.map(async (row) => {
    await recordFailure({
      paymentSessionId: row.id,
      customerId: row.customer_id,
      targetedMitraId: row.targeted_mitra_id,
      causeTag: PairingFailureCause.PAYMENT_SESSION_EXPIRED,
      amount: row.amount,
    })
    // Customer may be on searching/waiting; push terminal PAIRING_FAILED in real time.
    // FCM fallback when not WS-connected so they're notified at the OS level.
    try {
      const wsSent = sendToUser(UserType.CUSTOMER, row.customer_id, {
        type: WsMessage.PAIRING_FAILED,
        payment_session_id: row.id,
        cause_tag: PairingFailureCause.PAYMENT_SESSION_EXPIRED,
        is_terminal: true,
      })
      if (!wsSent) {
        await sendPushNotification(UserType.CUSTOMER, row.customer_id, {
          title: 'Sesi gagal',
          body: 'Sesi pembayaranmu telah berakhir. Silakan mulai ulang.',
          data: {
            type: WsMessage.PAIRING_FAILED,
            payment_session_id: row.id,
            cause_tag: PairingFailureCause.PAYMENT_SESSION_EXPIRED,
          },
        })
      }
    } catch (err) {
      console.error('expireStalePaymentSessions: failed to notify customer', {
        paymentSessionId: row.id, customerId: row.customer_id, err,
      })
    }
  }))

  return { expired: expired.length, failed: flipped.length }
}

export const getPaymentSession = async (id) => {
  const [row] = await sql`
    SELECT id, customer_id, amount, duration_minutes, is_first_session_discount, is_extension,
           mode, status, targeted_mitra_id, created_at, confirmed_at, consumed_at, expires_at
    FROM payment_sessions
    WHERE id = ${id}
  `
  return row || null
}

/**
 * Phase 4 Stage 10 — Chat Tab Pembayaran feed.
 *
 * Returns the customer's pending payment sessions (initial + extension) that
 * haven't paid AND haven't expired. The `expires_at > NOW()` filter is
 * defensive: the background sweeper flips stale pending rows to `expired`,
 * but rows can be stale between sweeps, so we filter inline too.
 *
 * Extension rows resolve mitra info via session_extensions → chat_sessions →
 * mitras. Initial rows fall back to `payment_sessions.targeted_mitra_id`
 * (set for targeted "Curhat lagi" flows); for general-blast initial rows
 * the mitra is unknown until pairing succeeds, so mitra fields are null.
 */
export const getCustomerPendingPayments = async (customerId) => {
  const items = await sql`
    SELECT
      ps.id,
      ps.is_extension,
      ps.amount,
      ps.duration_minutes,
      ps.mode,
      ps.created_at,
      ps.expires_at,
      COALESCE(ext_m.id, tgt_m.id)              AS mitra_id,
      COALESCE(ext_m.display_name, tgt_m.display_name) AS mitra_display_name
    FROM payment_sessions ps
    LEFT JOIN session_extensions se ON se.payment_session_id = ps.id
    LEFT JOIN chat_sessions cs ON cs.id = se.session_id
    LEFT JOIN mitras ext_m ON ext_m.id = cs.mitra_id
    LEFT JOIN mitras tgt_m ON tgt_m.id = ps.targeted_mitra_id
    WHERE ps.customer_id = ${customerId}
      AND ps.status = ${PaymentSessionStatus.PENDING}
      AND ps.expires_at > NOW()
    ORDER BY ps.created_at DESC
  `
  return { items, total: items.length }
}
