// Official contract: https://docs.bale.ai/ (wallet payments, not card-to-card).
// No provider response, token-bearing URL, or raw webhook is logged or returned.
type Json = Record<string, unknown>;
type Config = {url: string; key: string; allowedOrigins: string[]; fetcher?: typeof fetch};
type Settings = {version: number; bot_token: string; provider_token: string; webhook_secret: string; bot_username: string; test_mode: boolean; ready?: boolean};
const object = (value: unknown): value is Json => !!value && typeof value === 'object' && !Array.isArray(value);
const uuid = (value: unknown): value is string => typeof value === 'string' && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value);
const identifier = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= 200 && /^[!-~]+$/.test(value);
const integer = (value: unknown): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
const payer = (value: unknown): string | null => integer(value) ? String(value) : null;
// Pending review is resolved at the provider; only an authorized review may
// change its wallet outcome. Never send another invoice or re-inquire it.
const terminal = new Set(['pending_review', 'credited', 'test_paid', 'failed', 'cancelled']);
class PaymentError extends Error {
  status: number;
  constructor(code: string, status = 400) {super(code); this.status = status;}
}
export function baleAmountRial(input: unknown): string {
  if (typeof input !== 'string' || input.length > 60) throw new PaymentError('invalid_amount');
  const normalized = input.trim().replace(/[۰-۹]/g, c => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(c))).replace(/[٠-٩]/g, c => String('٠١٢٣٤٥٦٧٨٩'.indexOf(c))).replace(/[,٬\s]/g, '');
  if (!/^\d{1,12}$/.test(normalized) || BigInt(normalized) <= BigInt(0)) throw new PaymentError('invalid_amount');
  return (BigInt(normalized) * BigInt(10)).toString();
}
export function baleTransactionMatches(value: unknown, transaction: string, amount: string, user: string): value is Json {
  return object(value) && value.id === transaction && integer(value.amount) && String(value.amount) === amount && payer(value.userID) === user && ['pending', 'paid', 'failed', 'rejected'].includes(String(value.status));
}
async function equalSecret(actual: string, expected: string) {
  if (!expected || expected.length < 32 || actual.length > 200) return false;
  const hash = async (value: string) => new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)));
  const [a, b] = await Promise.all([hash(actual), hash(expected)]);
  let mismatch = 0; for (let i = 0; i < a.length; i++) mismatch |= a[i] ^ b[i];
  return mismatch === 0;
}
async function bodyJson(request: Request): Promise<Json> {
  if (!request.headers.get('content-type')?.startsWith('application/json')) throw new PaymentError('invalid_request', 415);
  const reader = request.body?.getReader(); if (!reader) throw new PaymentError('invalid_request');
  const chunks: Uint8Array[] = []; let length = 0;
  while (true) {const part = await reader.read(); if (part.done) break; length += part.value.length; if (length > 16000) {await reader.cancel(); throw new PaymentError('invalid_request', 413);} chunks.push(part.value);}
  const bytes = new Uint8Array(length); let offset = 0; for (const part of chunks) {bytes.set(part, offset); offset += part.length;}
  let value: unknown; try {value = JSON.parse(new TextDecoder().decode(bytes));} catch {throw new PaymentError('invalid_request');}
  if (!object(value)) throw new PaymentError('invalid_request'); return value;
}
async function responseJson(response: Response): Promise<unknown> {
  const reader = response.body?.getReader(); if (!reader) throw new PaymentError('payment_unavailable', 503);
  const chunks: Uint8Array[] = []; let length = 0;
  while (true) {const part = await reader.read(); if (part.done) break; length += part.value.length; if (length > 32768) {await reader.cancel(); throw new PaymentError('payment_unavailable', 503);} chunks.push(part.value);}
  const bytes = new Uint8Array(length); let offset = 0; for (const part of chunks) {bytes.set(part, offset); offset += part.length;}
  try {return JSON.parse(new TextDecoder().decode(bytes));} catch {throw new PaymentError('payment_unavailable', 503);}
}
export function balePaymentsHandler(config: Config) {
  const fetcher = config.fetcher || fetch;
  const base = config.url.replace(/\/$/, '');
  const reply = (body: unknown, status = 200) => Response.json(body, {status, headers: {'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'}});
  async function rpc<T = Json>(name: string, args: Json = {}, bearer = `Bearer ${config.key}`, timeout = 7000): Promise<T> {
    const response = await fetcher(`${base}/rest/v1/rpc/${name}`, {method: 'POST', redirect: 'error', headers: {apikey: config.key, Authorization: bearer, 'Content-Type': 'application/json'}, body: JSON.stringify(args), signal: AbortSignal.timeout(timeout)});
    const value = await responseJson(response);
    if (!response.ok) {
      const code = object(value) && typeof value.message === 'string' ? value.message : '';
      const allowed = /^(bale_[a-z_]+|test_deposit_limit|payment_rate_limit|payment_request_conflict|contact_not_verified|account_suspended|account_deleted|identity_required|not_authorized|not_authenticated|version_conflict|deposit_identity_required|gateway_unavailable|settings_conflict|payment_link_expired|payment_payer_conflict|payment_authority_conflict|payment_verification_mismatch|payments_in_progress|account_closed|payment_not_found|customer_account_suspended|customer_account_closed|customer_not_found)$/;
      throw new PaymentError(allowed.test(code) ? code : 'payment_unavailable', code.includes('rate_limit') ? 429 : response.status === 403 ? 403 : response.status >= 500 ? 503 : 409);
    }
    return value as T;
  }
  async function api(settings: Settings, method: 'getMe' | 'setWebhook' | 'getWebhookInfo' | 'sendInvoice' | 'answerPreCheckoutQuery' | 'inquireTransaction', args: Json = {}, timeout = 5000): Promise<unknown> {
    if (!settings.bot_token || /[\s/]/.test(settings.bot_token)) throw new PaymentError('bale_not_configured', 503);
    let response: Response;
    try {response = await fetcher(`https://tapi.bale.ai/bot${settings.bot_token}/${method}`, {method: 'POST', redirect: 'error', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(args), signal: AbortSignal.timeout(timeout)});} catch {throw new PaymentError('bale_provider_unavailable', 503);}
    let value: unknown; try {value = await responseJson(response);} catch {throw new PaymentError('bale_provider_unavailable', 503);}
    if (!response.ok || !object(value) || value.ok !== true) throw new PaymentError('bale_provider_unavailable', 503);
    return value.result;
  }
  function safePayment(record: Json) {
    if (record.test_mode === true && record.status === 'credited' || record.test_mode !== true && record.status === 'test_paid') throw new PaymentError('payment_unavailable', 503);
    return {paymentId: record.payment_id, status: record.status, testMode: record.test_mode === true, amountRial: typeof record.amount_rial === 'string' ? record.amount_rial : '', expiresAt: record.expires_at,
      reviewId: uuid(record.review_id) ? record.review_id : null, reviewRequired: !['credited', 'test_paid'].includes(String(record.status)) && (record.status === 'pending_review' || record.review_required === true),
      reviewDecision: ['pending', 'approved', 'rejected', 'problem'].includes(String(record.review_decision)) ? record.review_decision : null,
      ...(typeof record.start_token === 'string' && /^[A-Za-z0-9_-]{20,128}$/.test(record.start_token) && typeof record.bot_username === 'string' && /^[A-Za-z][A-Za-z0-9_]{2,63}$/.test(record.bot_username) && !terminal.has(String(record.status)) ? {url: `https://ble.ir/${record.bot_username}?start=${record.start_token}`} : {})};
  }
  async function reconcile(record: Json, settings: Settings) {
    if (terminal.has(String(record.status)) || !identifier(record.authority) || !identifier(record.payer) || !identifier(record.payload)) return;
    const transaction = await api(settings, 'inquireTransaction', {transaction_id: record.authority});
    if (!baleTransactionMatches(transaction, record.authority, String(record.amount_rial), record.payer)) throw new PaymentError('bale_verification_mismatch', 409);
    if (transaction.status === 'paid') {
      const settled = await rpc('marketplace_bale_payment_settle', {p_payload: record.payload, p_transaction: record.authority, p_reference: `bale:${record.authority}`, p_payer: record.payer, p_amount_rial: String(record.amount_rial), p_currency: 'IRR', p_verified_status: 'paid'});
      if (settled.ok !== true || !['pending_review', record.test_mode === true ? 'test_paid' : 'credited'].includes(String(settled.status))) throw new PaymentError('payment_unavailable', 503);
    } else if (transaction.status === 'failed' || transaction.status === 'rejected') {
      await rpc('marketplace_bale_payment_result', {p_payload: record.payload, p_transaction: record.authority, p_payer: record.payer, p_amount_rial: String(record.amount_rial), p_verified_status: transaction.status});
    }
  }
  async function webhook(request: Request, secret: string) {
    const current = await rpc<Settings>('marketplace_bale_config', {}, undefined, 1000);
    if (!await equalSecret(secret, current.webhook_secret)) throw new PaymentError('unauthorized', 401);
    const update = await bodyJson(request);
    if (!Number.isSafeInteger(update.update_id) || Number(update.update_id) < 0) throw new PaymentError('invalid_request');
    if (object(update.pre_checkout_query)) {
      const query = update.pre_checkout_query;
      if (!identifier(query.id)) throw new PaymentError('invalid_request');
      let accepted = false;
      let settings = current;
      try {
        if (!object(query.from) || !payer(query.from.id) || !integer(query.total_amount) || query.currency !== 'IRR' || !identifier(query.invoice_payload)) throw new PaymentError('invalid_request');
        const payment = await rpc('marketplace_bale_payment_lookup', {p_payload: query.invoice_payload}, undefined, 1000);
        settings = await rpc<Settings>('marketplace_bale_config', {p_payment: payment.payment_id}, undefined, 1000);
        await rpc('marketplace_bale_precheckout', {p_payload: query.invoice_payload, p_transaction: query.id, p_payer: payer(query.from.id), p_amount_rial: String(query.total_amount), p_currency: query.currency}, undefined, 1000);
        accepted = true;
      } catch {accepted = false;}
      // A pre-checkout approval is not a successful payment. Never credit here.
      await api(settings, 'answerPreCheckoutQuery', {pre_checkout_query_id: query.id, ok: accepted, ...(!accepted ? {error_message: 'این درخواست پرداخت معتبر نیست یا منقضی شده است. از پلانک درخواست تازه ایجاد کنید.'} : {})}, 4000);
      return reply({ok: true});
    }
    const message = update.message;
    if (!object(message) || !object(message.chat) || message.chat.type !== 'private' || !object(message.from) || !payer(message.from.id) || !payer(message.chat.id) || message.chat.id !== message.from.id) return reply({ok: true});
    if (object(message.successful_payment)) {
      const payment = message.successful_payment;
      if (payment.currency !== 'IRR' || !integer(payment.total_amount) || !identifier(payment.invoice_payload) || !identifier(payment.telegram_payment_charge_id)) throw new PaymentError('invalid_request');
      const record = await rpc('marketplace_bale_payment_lookup', {p_payload: payment.invoice_payload});
      if (record.authority !== payment.telegram_payment_charge_id || String(record.amount_rial) !== String(payment.total_amount) || record.payer !== payer(message.from.id)) throw new PaymentError('bale_verification_mismatch', 409);
      if (terminal.has(String(record.status))) return reply({ok: true});
      const settings = await rpc<Settings>('marketplace_bale_config', {p_payment: record.payment_id});
      await reconcile(record, settings);
      return reply({ok: true});
    }
    if (typeof message.text === 'string') {
      const match = /^\/start(?:@[A-Za-z0-9_]+)? ([A-Za-z0-9_-]{20,128})$/.exec(message.text.trim());
      if (!match) return reply({ok: true});
      const claim = await rpc('marketplace_bale_invoice_claim', {p_start_token: match[1], p_payer: payer(message.from.id), p_chat: payer(message.chat.id)});
      if (claim.should_send !== true) return reply({ok: true});
      const settings = await rpc<Settings>('marketplace_bale_config', {p_payment: claim.payment_id});
      if (typeof claim.test_mode !== 'boolean' || settings.test_mode !== claim.test_mode) throw new PaymentError('bale_verification_mismatch', 409);
      const amount = Number(claim.amount_rial);
      if (!integer(amount) || !identifier(claim.payload)) throw new PaymentError('invalid_request');
      // Claim is committed before contacting Bale. Never blindly resend after a timeout:
      // the provider may have delivered the invoice despite losing its response.
      const sent = await api(settings, 'sendInvoice', {chat_id: message.chat.id, title: 'شارژ کیف پول پلانک', description: settings.test_mode ? 'پرداخت آزمایشی؛ موجودی واقعی کیف پول تغییر نمی‌کند.' : 'ثبت درخواست افزایش موجودی تومان؛ شارژ کیف پول پس از تأیید درخواست واریز انجام می‌شود.', payload: claim.payload, provider_token: settings.test_mode ? 'WALLET-TEST-1111111111111111' : settings.provider_token, prices: [{label: 'شارژ کیف پول پلانک', amount}]});
      if (!object(sent) || !integer(sent.message_id)) throw new PaymentError('bale_provider_unavailable', 503);
      await rpc('marketplace_bale_invoice_sent', {p_payment: claim.payment_id, p_message_id: String(sent.message_id)});
    }
    return reply({ok: true});
  }
  return async (request: Request): Promise<Response> => {
    try {
      if (request.method !== 'POST') return reply({error: 'method_not_allowed'}, 405);
      if (!base || !config.key) throw new PaymentError('payment_unavailable', 503);
      const path = new URL(request.url).pathname;
      const webhookPath = /\/bale-payments\/webhook\/([A-Za-z0-9_-]+)$/.exec(path);
      if (webhookPath) return await webhook(request, webhookPath[1]);
      if (!path.endsWith('/bale-payments')) throw new PaymentError('not_found', 404);
      const bearer = request.headers.get('authorization'); if (!bearer?.startsWith('Bearer ')) throw new PaymentError('unauthorized', 401);
      const body = await bodyJson(request);
      if (typeof body.origin !== 'string' || !config.allowedOrigins.includes(body.origin) || request.headers.get('origin') && !config.allowedOrigins.includes(request.headers.get('origin')!)) throw new PaymentError('origin_not_allowed', 403);
      const auth = await fetcher(`${base}/auth/v1/user`, {redirect: 'error', headers: {apikey: config.key, Authorization: bearer}, signal: AbortSignal.timeout(7000)});
      const user = auth.ok ? await responseJson(auth) : null; if (!object(user) || !uuid(user.id)) throw new PaymentError('unauthorized', 401);
      if (body.action === 'configure' || body.action === 'reconcile_pending') {
        // Run with the user's JWT, not the service role. The RPC enforces owner role/MFA policy.
        await rpc('marketplace_bale_settings', {}, bearer);
        if (body.action === 'reconcile_pending') {
          const batch = await rpc<Json[]>('marketplace_bale_pending_payments', {p_limit: 5});
          if (!Array.isArray(batch) || batch.length > 5) throw new PaymentError('payment_unavailable', 503);
          const result = {ok: true, checked: batch.length, settled: 0, pending: 0, failed: 0, errors: 0};
          // A small parallel batch bounds wall time; each payment also has a database inquiry lease.
          await Promise.all(batch.map(async record => {
            try {
              if (!object(record) || !uuid(record.payment_id) || !identifier(record.payload)) throw new PaymentError('payment_unavailable', 503);
              if (terminal.has(String(record.status))) {
                if (record.status === 'credited' || record.status === 'test_paid') result.settled++;
                else if (record.status === 'failed') result.failed++;
                else result.pending++;
                return;
              }
              if (!await rpc<boolean>('marketplace_bale_reconcile_claim', {p_payment: record.payment_id})) {result.pending++; return;}
              const settings = await rpc<Settings>('marketplace_bale_config', {p_payment: record.payment_id});
              await reconcile(record, settings);
              const latest = await rpc('marketplace_bale_payment_lookup', {p_payload: record.payload});
              if (latest.status === 'credited' || latest.status === 'test_paid') result.settled++;
              else if (latest.status === 'failed') result.failed++;
              else result.pending++;
            } catch {result.errors++;}
          }));
          return reply(result);
        }
        const settings = await rpc<Settings>('marketplace_bale_config');
        try {
          const me = await api(settings, 'getMe');
          if (!object(me) || me.is_bot !== true || !payer(me.id) || me.username !== settings.bot_username) throw new PaymentError('bale_bot_mismatch', 409);
          if (!/^[A-Za-z0-9_-]{32,128}$/.test(settings.webhook_secret)) throw new PaymentError('bale_not_configured', 503);
          const target = `${base}/functions/v1/bale-payments/webhook/${settings.webhook_secret}`;
          if (new URL(target).protocol !== 'https:') throw new PaymentError('bale_https_required', 409);
          if (await api(settings, 'setWebhook', {url: target}) !== true) throw new PaymentError('bale_webhook_unverified', 503);
          const webhookInfo = await api(settings, 'getWebhookInfo');
          if (!object(webhookInfo) || webhookInfo.url !== target) throw new PaymentError('bale_webhook_unverified', 503);
          await rpc('marketplace_bale_connection_record', {p_version: settings.version, p_bot_id: payer(me.id), p_bot_username: me.username, p_ready: true, p_error_code: null});
          return reply({ok: true, ready: true, botUsername: me.username, checkedAt: new Date().toISOString()});
        } catch (error) {
          await rpc('marketplace_bale_connection_record', {p_version: settings.version, p_bot_id: null, p_bot_username: settings.bot_username, p_ready: false, p_error_code: error instanceof PaymentError && error.message === 'bale_bot_mismatch' ? 'bot_mismatch' : error instanceof PaymentError && error.message === 'bale_webhook_unverified' ? 'webhook_mismatch' : 'provider_unavailable'}).catch(() => undefined);
          throw error;
        }
      }
      if (body.action === 'start') {
        if (!uuid(body.requestId)) throw new PaymentError('invalid_request');
        const record = await rpc('marketplace_bale_payment_create', {p_user: user.id, p_request: body.requestId, p_amount_rial: baleAmountRial(body.amountToman)});
        return reply(safePayment(record));
      }
      if (body.action === 'status') {
        if (!uuid(body.paymentId)) throw new PaymentError('invalid_request');
        const args = {p_payment: body.paymentId, p_user: user.id};
        const record = await rpc('marketplace_bale_payment_status', args);
        if (!terminal.has(String(record.status)) && record.authority && await rpc<boolean>('marketplace_bale_reconcile_claim', {p_payment: body.paymentId})) {
          const settings = await rpc<Settings>('marketplace_bale_config', {p_payment: body.paymentId});
          await reconcile(record, settings);
          return reply(safePayment(await rpc('marketplace_bale_payment_status', args)));
        }
        return reply(safePayment(record));
      }
      throw new PaymentError('invalid_request');
    } catch (error) {
      return error instanceof PaymentError ? reply({error: error.message}, error.status) : reply({error: 'payment_unavailable'}, 503);
    }
  };
}
