// Server-only adapter for https://www.tcartt.com/docs (Partner API).
// Mirrored in planck-admin so reconciliation never depends on a running storefront.
// A browser redirect is not evidence; only an authenticated invoice GET records proof.
if (typeof window !== 'undefined') throw new Error('Server-only payment adapter');

type Json = Record<string, unknown>;
type Config = {url: string; key: string; fetcher?: typeof fetch};
type StatusInput = {paymentId?: string; requestId?: string};
export class TcarttServiceError extends Error {
  code: string; status: number;
  constructor(code = 'tcartt_unavailable', status = 503) {super(code); this.code = code; this.status = status;}
}
const object = (v: unknown): v is Json => !!v && typeof v === 'object' && !Array.isArray(v);
const uuid = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v);
const positiveInteger = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
const invoiceId = (v: unknown): string | null => positiveInteger(v) ? String(v) : typeof v === 'string' && /^[1-9]\d{0,15}$/.test(v) && Number.isSafeInteger(Number(v)) ? v : null;
const rial = (v: unknown): v is string => typeof v === 'string' && /^\d{1,13}$/.test(v) && BigInt(v) >= BigInt(10000) && BigInt(v) <= BigInt('9999999999990') && BigInt(v) % BigInt(10) === BigInt(0);
const terminal = new Set(['pending_review', 'credited', 'expired', 'cancelled', 'failed']);
const timestamp = (v: unknown): v is string => typeof v === 'string' && v.length <= 50 && Number.isFinite(Date.parse(v));
function providerDate(v: unknown): string | null {
  // Examples omit a zone. Normalize consistently to UTC pending live contract
  // validation; preserve an explicit offset and never use the device timezone.
  if (typeof v !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,6})?(?:Z|[+-]\d\d:\d\d)?$/.test(v)) return null;
  const zoned = /(?:Z|[+-]\d\d:\d\d)$/.test(v) ? v : v + 'Z';
  return timestamp(zoned) ? new Date(zoned).toISOString() : null;
}
export function tcarttAmountToman(v: unknown): number {
  if (typeof v !== 'string' || v.length > 60) throw new TcarttServiceError('invalid_amount', 400);
  const n = v.trim().replace(/[۰-۹]/g, c => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(c))).replace(/[٠-٩]/g, c => String('٠١٢٣٤٥٦٧٨٩'.indexOf(c))).replace(/[,٬\s]/g, '');
  if (!/^\d{1,12}$/.test(n) || Number(n) < 1000) throw new TcarttServiceError('invalid_amount', 400);
  return Number(n);
}
function paymentUrl(v: unknown, expectedId?: string): string | null {
  if (typeof v !== 'string' || v.length > 1000) return null;
  try {
    const u = new URL(v), id = /^\/order\/([1-9]\d*)$/.exec(u.pathname)?.[1];
    if (u.protocol !== 'https:' || !['tcartt.com', 'www.tcartt.com'].includes(u.hostname) || u.username || u.password || u.port || u.hash || !id
      || expectedId && id !== expectedId || [...u.searchParams.keys()].length !== 1 || !/^[A-Za-z0-9_.~-]{1,512}$/.test(u.searchParams.get('t') || '')) return null;
    return u.href;
  } catch {return null;}
}
export function tcarttSafePayment(v: unknown) {
  if (!object(v) || !uuid(v.payment_id) || !rial(v.base_amount_rial) || !rial(v.amount_rial) || BigInt(v.amount_rial) < BigInt(v.base_amount_rial)
    || !['creating', 'creation_unknown', 'pending', ...terminal].includes(String(v.status))) throw new TcarttServiceError();
  const status = v.status === 'creating' ? 'creation_unknown' : String(v.status);
  const id = invoiceId(v.invoice_id);
  const url = status === 'pending' && id ? paymentUrl(v.payment_url, id) : null;
  if (status === 'pending' && !url || v.expires_at != null && !timestamp(v.expires_at)
    || v.tracking_code != null && (typeof v.tracking_code !== 'string' || !/^[A-Za-z0-9_-]{1,100}$/.test(v.tracking_code))
    || v.review_decision != null && (typeof v.review_decision !== 'string' || !['pending', 'approved', 'rejected', 'problem'].includes(v.review_decision))) throw new TcarttServiceError();
  return {paymentId: v.payment_id, status, url, baseAmountRial: v.base_amount_rial, amountRial: v.amount_rial,
    expiresAt: typeof v.expires_at === 'string' ? v.expires_at : null, trackingCode: typeof v.tracking_code === 'string' ? v.tracking_code : null,
    reviewDecision: typeof v.review_decision === 'string' ? v.review_decision : null};
}
async function boundedJson(response: Response): Promise<unknown> {
  const reader = response.body?.getReader(); if (!reader) throw new TcarttServiceError();
  const chunks: Uint8Array[] = []; let size = 0;
  while (true) {const {done, value} = await reader.read(); if (done) break; size += value.length; if (size > 32768) {await reader.cancel(); throw new TcarttServiceError();} chunks.push(value);}
  const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) {bytes.set(chunk, offset); offset += chunk.length;}
  try {return JSON.parse(new TextDecoder().decode(bytes));} catch {throw new TcarttServiceError();}
}
const rpcCodes: Record<string, number> = {
  payment_not_found: 404, payment_request_conflict: 409, payment_invoice_conflict: 409, payment_verification_mismatch: 409,
  payment_not_pending: 409, payment_rate_limit: 429, invalid_amount: 400, invalid_request: 400, invalid_tcartt_invoice: 409,
  not_authenticated: 401, not_authorized: 403, contact_not_verified: 403, customer_account_suspended: 403, customer_account_closed: 403,
  account_suspended: 403, account_closed: 403, customer_not_found: 403, tcartt_not_configured: 503,
};

export function createTcarttService(config: Config) {
  const fetcher = config.fetcher || fetch;
  let base: string;
  try {const u = new URL(config.url); if (u.protocol !== 'https:' || u.username || u.password || u.port || u.search || u.hash || !['', '/'].includes(u.pathname) || !config.key || /[\r\n]/.test(config.key)) throw new Error(); base = u.origin;}
  catch {throw new TcarttServiceError('tcartt_server_not_configured');}
  async function rpc(name: string, body: Json = {}): Promise<unknown> {
    let response: Response;
    try {response = await fetcher(`${base}/rest/v1/rpc/${name}`, {method: 'POST', redirect: 'error', cache: 'no-store',
      headers: {apikey: config.key, ...(config.key.startsWith('sb_secret_') ? {} : {Authorization: `Bearer ${config.key}`}), 'Content-Type': 'application/json'},
      body: JSON.stringify(body), signal: AbortSignal.timeout(10000)});} catch {throw new TcarttServiceError();}
    const value = await boundedJson(response);
    if (!response.ok) {
      if (object(value) && value.code === 'PGRST202') throw new TcarttServiceError('tcartt_database_not_ready');
      const code = object(value) && typeof value.message === 'string' ? value.message : '';
      throw new TcarttServiceError(Object.hasOwn(rpcCodes, code) ? code : 'tcartt_unavailable', rpcCodes[code] || 503);
    }
    return value;
  }
  async function settings(payment: string) {
    const value = await rpc('marketplace_tcartt_config', {p_payment: payment});
    if (!object(value) || typeof value.api_key !== 'string' || value.api_key.length < 8 || value.api_key.length > 8192 || !/^[!-~]+$/.test(value.api_key)) throw new TcarttServiceError('tcartt_not_configured');
    return value.api_key;
  }
  async function api(key: string, path: string, body?: Json): Promise<Json> {
    let response: Response;
    try {response = await fetcher('https://www.tcartt.com/api/v1/' + path, {method: body ? 'POST' : 'GET', redirect: 'error', cache: 'no-store',
      headers: {Authorization: `Bearer ${key}`, Accept: 'application/json', ...(body ? {'Content-Type': 'application/json'} : {})},
      ...(body ? {body: JSON.stringify(body)} : {}), signal: AbortSignal.timeout(12000)});} catch {throw new TcarttServiceError('tcartt_provider_unavailable');}
    if (response.status === 401 || response.status === 403) {await response.body?.cancel(); throw new TcarttServiceError('tcartt_provider_not_ready');}
    const value = await boundedJson(response);
    if (!response.ok || !object(value) || value.ok !== true) throw new TcarttServiceError('tcartt_provider_unavailable');
    return value;
  }
  function owned(value: unknown, user?: string): Json {
    if (!object(value) || !uuid(value.payment_id) || !uuid(value.user_id) || user && value.user_id !== user) throw new TcarttServiceError('payment_not_found', 404);
    tcarttSafePayment(value); return value;
  }
  async function lookup(user: string, input: StatusInput): Promise<Json> {
    if (!uuid(user) || (input.paymentId === undefined) === (input.requestId === undefined) || input.paymentId !== undefined && !uuid(input.paymentId) || input.requestId !== undefined && !uuid(input.requestId)) throw new TcarttServiceError('invalid_request', 400);
    return owned(await rpc('marketplace_tcartt_payment_status', {p_user: user, p_payment: input.paymentId ?? null, p_request: input.requestId ?? null}), user);
  }
  function matches(invoice: Json, record: Json) {
    return invoiceId(invoice.invoice_id) === record.invoice_id && invoice.external_order_id === record.payment_id
      && positiveInteger(invoice.base_price_toman) && String(invoice.base_price_toman * 10) === record.base_amount_rial
      && positiveInteger(invoice.unique_amount_toman) && String(invoice.unique_amount_toman * 10) === record.amount_rial
      && (invoice.buyer_ref === undefined || invoice.buyer_ref === record.payment_id)
      && ['pending', 'paid', 'expired', 'cancelled'].includes(String(invoice.status));
  }
  async function reconcile(record: Json): Promise<Json> {
    if (terminal.has(String(record.status)) || !invoiceId(record.invoice_id)) return record;
    if (await rpc('marketplace_tcartt_reconcile_claim', {p_payment: record.payment_id}) !== true) return record;
    const invoice = await api(await settings(String(record.payment_id)), 'invoices/' + record.invoice_id);
    if (!matches(invoice, record)) throw new TcarttServiceError('payment_verification_mismatch', 409);
    const args = {p_payment: record.payment_id, p_invoice_id: record.invoice_id, p_external_order_id: invoice.external_order_id,
      p_base_price_toman: invoice.base_price_toman, p_unique_amount_toman: invoice.unique_amount_toman, p_verified_status: invoice.status};
    if (invoice.status === 'paid') {
      const paidAt = providerDate(invoice.paid_at); if (!paidAt) throw new TcarttServiceError('payment_verification_mismatch', 409);
      await rpc('marketplace_tcartt_payment_settle', {...args, p_paid_at: paidAt});
    } else if (invoice.status !== 'pending') await rpc('marketplace_tcartt_payment_result', args);
    return lookup(String(record.user_id), {paymentId: String(record.payment_id)});
  }
  return {
    async start(user: string, input: {requestId: string; amountToman: string}, origin: string) {
      if (!uuid(user) || !uuid(input.requestId)) throw new TcarttServiceError('invalid_request', 400);
      // The route supplies its validated configured origin, never a browser return URL.
      const returnUrl = new URL('/wallet/deposit', origin);
      if (returnUrl.origin !== origin || !(returnUrl.protocol === 'https:' || returnUrl.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(returnUrl.hostname))) throw new TcarttServiceError('invalid_request', 400);
      const amount = tcarttAmountToman(input.amountToman);
      const record = owned(await rpc('marketplace_tcartt_payment_claim', {p_user: user, p_request: input.requestId, p_amount_toman: amount}), user);
      if (record.should_create !== true) return tcarttSafePayment(record);
      returnUrl.searchParams.set('tcartt_payment', String(record.payment_id));
      let invoiceRequested = false;
      try {
        const key = await settings(String(record.payment_id));
        invoiceRequested = true;
        const invoice = await api(key, 'invoices', {amount_toman: amount, title: 'واریز به کیف پول پلانک',
          external_order_id: record.payment_id, buyer_ref: record.payment_id, return_url: returnUrl.href, expires_minutes: 30});
        const id = invoiceId(invoice.invoice_id), url = id ? paymentUrl(invoice.payment_url, id) : null, expires = providerDate(invoice.expires_at);
        if (!id || !url || !expires || invoice.status !== 'pending' || invoice.external_order_id !== record.payment_id
          || invoice.base_price_toman !== amount || !positiveInteger(invoice.unique_amount_toman) || invoice.unique_amount_toman < amount || invoice.unique_amount_toman > 999999999999
          || typeof invoice.tracking_code !== 'string' || !/^[A-Za-z0-9_-]{1,100}$/.test(invoice.tracking_code)) throw new TcarttServiceError('tcartt_invalid_invoice');
        await rpc('marketplace_tcartt_payment_attach', {p_payment: record.payment_id, p_invoice_id: id, p_tracking_code: invoice.tracking_code,
          p_payment_url: url, p_unique_amount_toman: invoice.unique_amount_toman, p_expires_at: expires});
        return tcarttSafePayment(await lookup(user, {paymentId: String(record.payment_id)}));
      } catch (error) {
        // A request never sent, or explicitly refused authentication, cannot
        // have created an invoice. Release this failed attempt for key repair.
        if (!invoiceRequested || error instanceof TcarttServiceError && error.code === 'tcartt_provider_not_ready') {
          await rpc('marketplace_tcartt_creation_failed', {p_payment: record.payment_id});
          throw error instanceof TcarttServiceError ? error : new TcarttServiceError();
        }
        // Do not repeat a POST after a timeout: there may already be a payable invoice.
        // The request ID and unknown state survive so support can reconcile it.
        await rpc('marketplace_tcartt_payment_unknown', {p_payment: record.payment_id}).catch(() => undefined);
        throw new TcarttServiceError('tcartt_creation_unknown', 503);
      }
    },
    async status(user: string, input: StatusInput) {return tcarttSafePayment(await reconcile(await lookup(user, input)));},
    async reconcilePending() {
      const batch = await rpc('marketplace_tcartt_pending_payments', {p_limit: 5});
      if (!Array.isArray(batch) || batch.length > 5) throw new TcarttServiceError();
      const result = {checked: batch.length, verified: 0, pending: 0, errors: 0, hasMore: batch.length === 5};
      await Promise.all(batch.map(async item => {
        try {const value = await reconcile(owned(item)); if (['pending_review', 'credited'].includes(String(value.status))) result.verified++; else result.pending++;}
        catch {result.errors++;}
      }));
      return result;
    },
  };
}
