/**
 * DID Connect authentication for Cloudflare Workers.
 *
 * Since the published @arcblock/did-connect-js@1.28.0 does not include the Hono adapter,
 * we port the adapter logic here. When a newer version with native Hono support ships,
 * this file can be replaced by a direct import.
 *
 * Reference: blockchain/did/did-connect/src/adapters/hono.ts
 */
import { WalletAuthenticator, WalletHandlers } from '@arcblock/did-connect-js';
// abt-wallet 4.20+ is dual-decode (both CBOR + protobuf), so we can go back
// to @ocap/client/encode's CBOR-only txEncoder. Drops ~300KB google-protobuf
// runtime from the worker bundle. Requires abt-wallet >= 4.20.
// fetchContext is also exported so we can pre-warm the module-level cache
// at isolate init time — avoids the first-request 8s merchant timeout that
// comes from fetchContext hitting beta chain (2 GraphQL queries in parallel).
import { createTxEncoder, fetchContext } from '@ocap/client/encode';
import * as Mcrypto from '@ocap/mcrypto';
import { fromSecretKey } from '@ocap/wallet';
import { EventEmitter } from 'events';

// Import original connect handlers — esbuild aliases handle all dependency replacements
import collectHandlers from '../api/src/routes/connect/collect';
import collectBatchHandlers from '../api/src/routes/connect/collect-batch';
import payHandlers from '../api/src/routes/connect/pay';
import setupHandlers from '../api/src/routes/connect/setup';
import subscribeHandlers from '../api/src/routes/connect/subscribe';
import changePaymentHandlers from '../api/src/routes/connect/change-payment';
import changePlanHandlers from '../api/src/routes/connect/change-plan';
import changePayerHandlers from '../api/src/routes/connect/change-payer';
import rechargeHandlers from '../api/src/routes/connect/recharge';
import rechargeAccountHandlers from '../api/src/routes/connect/recharge-account';
import delegationHandlers from '../api/src/routes/connect/delegation';
import overdraftProtectionHandlers from '../api/src/routes/connect/overdraft-protection';
import reStakeHandlers from '../api/src/routes/connect/re-stake';
import autoRechargeAuthHandlers from '../api/src/routes/connect/auto-recharge-auth';

const walletType = {
  role: Mcrypto.types.RoleType.ROLE_APPLICATION,
  pk: Mcrypto.types.KeyType.ED25519,
  hash: Mcrypto.types.HashType.SHA3,
};

// ---------------------------------------------------------------------------
// CloudflareKVStorage – ported from blockchain/did/did-connect/src/storage/kv.ts
// Not yet published in @arcblock/did-connect-js@1.28.0
// ---------------------------------------------------------------------------

// D1-based storage for DID Connect tokens.
// Replaces KV which is eventually consistent across datacenters (up to 60s delay).
// D1 is strongly consistent — writes are immediately visible to all readers.
// This fixes mobile wallet scanning: wallet POST auth writes to datacenter A,
// browser status polling reads from datacenter B, and sees the update immediately.
class CloudflareD1Storage extends EventEmitter {
  private ttl: number;

  constructor(options: { ttl?: number } = {}) {
    super();
    this.ttl = options.ttl ?? 300;
  }

  private _getDB() {
    const env = (globalThis as any).__CF_ENV__;
    const db = env?.DB;
    if (!db) {
      console.error('[D1Storage] DB not available - __CF_ENV__ missing or no DB binding');
      return db;
    }
    return db.withSession('first-primary');
  }

  async create(token: string, status = 'created') {
    try {
      const db = this._getDB();
      if (!db) throw new Error('DB not available');
      const record = { token, status };
      const expiresAt = Math.floor(Date.now() / 1000) + this.ttl;
      await db
        .prepare('INSERT OR REPLACE INTO _did_connect_tokens (token, data, expires_at) VALUES (?, ?, ?)')
        .bind(token, JSON.stringify(record), expiresAt)
        .run();
      this.emit('create', record);
      return record;
    } catch (err: any) {
      console.error('[D1Storage] create failed:', token, err?.message || err);
      throw err;
    }
  }

  async read(token: string) {
    try {
      const db = this._getDB();
      if (!db) return null;
      const now = Math.floor(Date.now() / 1000);
      const row = await db
        .prepare('SELECT data FROM _did_connect_tokens WHERE token = ? AND expires_at > ?')
        .bind(token, now)
        .first();
      if (!row) return null;
      return JSON.parse(row.data as string);
    } catch (err: any) {
      console.error('[D1Storage] read failed:', token, err?.message || err);
      return null;
    }
  }

  update(token: string, updates: Record<string, any>) {
    // did-connect-js's handlers/util.ts onProcessError calls tokenStorage.update
    // without awaiting it (fire-and-forget). On CF Workers a non-awaited Promise
    // is terminated when the request handler returns, so the UPDATE never hits
    // D1 and the token stays at "scanned" forever — the wallet UI then loops on
    // `status: scanned` and shows "validating..." indefinitely.
    //
    // We wrap the real write in a promise, and register it via the global
    // ctx.waitUntil (exposed as __cfWaitUntil__ in worker.ts) so the runtime
    // keeps the isolate alive until the D1 write finishes, regardless of
    // whether the caller awaits us.
    const promise = (async () => {
      try {
        const existing = await this.read(token);
        if (!existing) return null;

        delete updates.token;
        const merged = { ...existing, ...updates };
        const expiresAt = Math.floor(Date.now() / 1000) + this.ttl;
        const db = this._getDB();
        if (!db) throw new Error('DB not available');
        await db
          .prepare('UPDATE _did_connect_tokens SET data = ?, expires_at = ? WHERE token = ?')
          .bind(JSON.stringify(merged), expiresAt, token)
          .run();
        this.emit('update', merged);
        return merged;
      } catch (err: any) {
        console.error('[D1Storage] update failed:', token, err?.message || err);
        throw err;
      }
    })();

    const waitUntil = (globalThis as any).__cfWaitUntil__ as ((p: Promise<any>) => void) | undefined;
    if (typeof waitUntil === 'function') {
      // Swallow rejection in waitUntil — the inner promise will still reject
      // for any caller that does await us.
      waitUntil(promise.catch(() => {}));
    }
    return promise;
  }

  async delete(token: string) {
    try {
      const existing = await this.read(token);
      if (existing) {
        this.emit('destroy', existing);
      }
      const db = this._getDB();
      if (!db) return;
      await db.prepare('DELETE FROM _did_connect_tokens WHERE token = ?').bind(token).run();
    } catch (err: any) {
      console.error('[D1Storage] delete failed:', token, err?.message || err);
    }
  }

  async exist(token: string, did?: string) {
    const record = await this.read(token);
    if (!record) return false;
    if (did) return record.did === did;
    return true;
  }
}

// ---------------------------------------------------------------------------
// Hono adapter – wraps Express-style (req, res, next) handlers for Hono
// ---------------------------------------------------------------------------

function parseCookieHeader(header: string): Record<string, string> {
  const cookies: Record<string, string> = {};
  if (!header) return cookies;
  for (const pair of header.split(';')) {
    const idx = pair.indexOf('=');
    if (idx > 0) {
      const key = pair.slice(0, idx).trim();
      const val = pair.slice(idx + 1).trim();
      cookies[key] = decodeURIComponent(val);
    }
  }
  return cookies;
}

function negotiateLanguage(acceptHeader: string, ...langs: string[]): string | false {
  if (!acceptHeader || langs.length === 0) return langs[0] || false;
  const lower = acceptHeader.toLowerCase();
  for (const lang of langs) {
    const prefix = lang.toLowerCase().split('-')[0];
    if (lower.includes(lang.toLowerCase()) || lower.includes(prefix)) {
      return lang;
    }
  }
  return langs[0] || false;
}

function createHonoRequest(c: any, bodyCache: any = {}): any {
  const url = new URL(c.req.url);
  const rawHeaders: Record<string, string> = {};
  if (c.req.raw?.headers) {
    c.req.raw.headers.forEach((value: string, key: string) => {
      rawHeaders[key.toLowerCase()] = value;
    });
  }

  return {
    body: bodyCache,
    query: Object.fromEntries(url.searchParams.entries()),
    params: typeof c.req.param === 'function' ? c.req.param() : {},
    headers: rawHeaders,
    cookies: parseCookieHeader(rawHeaders.cookie || ''),
    protocol: url.protocol.replace(':', ''),
    originalUrl: url.pathname + url.search,
    get(name: string) {
      return rawHeaders[name.toLowerCase()];
    },
    header(name: string) {
      return rawHeaders[name.toLowerCase()];
    },
    acceptsLanguages(...langs: string[]) {
      return negotiateLanguage(rawHeaders['accept-language'] || '', ...langs);
    },
    raw: c,
  };
}

function createHonoResponse(): any {
  let result: { statusCode: number; body: any } | null = null;

  return {
    jsonp(data: any) {
      result = { statusCode: 200, body: data };
    },
    json(data: any) {
      result = { statusCode: 200, body: data };
    },
    status(code: number) {
      return {
        json(data: any) {
          result = { statusCode: code, body: data };
        },
      };
    },
    _getResult() {
      return result;
    },
  };
}

/**
 * Wrap a chain of Express-style middlewares + handler into a single Hono handler.
 */
function wrapHandler(
  middlewares: Array<(req: any, res: any, next: () => void) => any>,
  handler: (req: any, res: any) => Promise<void>
) {
  return async (c: any) => {
    // Ensure __CF_ENV__ is available for D1Storage
    if (!(globalThis as any).__CF_ENV__) {
      (globalThis as any).__CF_ENV__ = c.env;
    }
    let body = {};
    try {
      const text = await c.req.text();
      if (text) {
        const contentType = c.req.header('content-type') || '';
        if (contentType.includes('json')) {
          body = JSON.parse(text);
        } else if (contentType.includes('urlencoded')) {
          body = Object.fromEntries(new URLSearchParams(text).entries());
        }
      }
    } catch {
      // Body parsing failed
    }

    const req = createHonoRequest(c, body);
    const res = createHonoResponse();

    // Run middlewares
    for (const mw of middlewares) {
      let nextCalled = false;
      await mw(req, res, () => {
        nextCalled = true;
      });
      if (!nextCalled) {
        const r = res._getResult();
        if (r) return c.json(r.body, r.statusCode);
        return c.json({ error: 'Unknown error' }, 500);
      }
    }

    // Run handler
    try {
      await handler(req, res);
    } catch (err: any) {
      console.error(
        '[DID Connect] handler error:',
        err?.message || err,
        err?.stack?.split('\n').slice(0, 5).join('\n')
      );
      // Write error to D1 for debugging (console.error may not be visible in tail)
      try {
        const db = (globalThis as any).__CF_ENV__?.DB;
        if (db) {
          await db
            .prepare('INSERT OR REPLACE INTO _locks (name, owner, expires_at) VALUES (?, ?, 0)')
            .bind('debug-did-error', `${err?.message || err}`.substring(0, 200))
            .run();
        }
      } catch {
        /* ignore */
      }
      return c.json({ error: err?.message || 'Internal server error' }, 500);
    }
    const r = res._getResult();
    return c.json(r?.body ?? {}, r?.statusCode ?? 200);
  };
}

/**
 * Monkey-patch the WalletHandlers.attach method to support Hono apps.
 *
 * The published @arcblock/did-connect-js expects Express. We intercept attach()
 * and register routes on the Hono app directly using wrapHandler().
 */
function createHonoCompatHandlers(opts: { tokenStorage: any; authenticator: any }): WalletHandlers {
  const handlers = new WalletHandlers(opts);
  const originalAttach = handlers.attach.bind(handlers);

  // Override attach to use Hono-compatible route registration
  (handlers as any).attach = function attachHono(config: any) {
    const { app, action, ...rest } = config;

    // Create a fake Express-like app that collects routes
    const routes: Array<{
      method: string;
      path: string;
      middlewares: Function[];
      handler: Function;
    }> = [];

    const fakeApp: any = {
      get(path: string, ...fns: Function[]) {
        routes.push({ method: 'get', path, middlewares: fns.slice(0, -1), handler: fns[fns.length - 1] });
      },
      post(path: string, ...fns: Function[]) {
        routes.push({ method: 'post', path, middlewares: fns.slice(0, -1), handler: fns[fns.length - 1] });
      },
      use(_path: string, _middleware: any) {
        // CORS is handled by Hono's cors middleware already
      },
    };

    // Call original attach with fake app to collect routes
    originalAttach({ app: fakeApp, action, ...rest });

    // Now register collected routes on the real Hono app
    const prefix = (handlers as any).options?.prefix || '/api/did';

    // Add CORS for DID Connect routes
    app.use(`${prefix}/${action}/*`, async (c: any, next: () => Promise<void>) => {
      c.header('Access-Control-Allow-Origin', '*');
      c.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
      c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
      if (c.req.method === 'OPTIONS') {
        return c.text('', 204);
      }
      await next();
    });

    for (const route of routes) {
      const wrapped = wrapHandler(route.middlewares as any[], route.handler as any);
      if (route.method === 'get') {
        app.get(route.path, wrapped);
      } else if (route.method === 'post') {
        app.post(route.path, wrapped);
      }
    }
  };

  return handlers;
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

export function initAuth(kv: KVNamespace, appSK: string) {
  const wallet = fromSecretKey(appSK, walletType);
  const tokenStorage = new CloudflareD1Storage({ ttl: 300 });
  // Trailing slash is REQUIRED — without it the host 50% times out (verified 2026-04-20).
  // Matches the format used by D1 payment_methods.settings.arcblock.api_host.
  const chainHost = 'https://beta.abtnetwork.io/api/';

  // Pre-warm @ocap/client/encode's module-level context cache. The first
  // createTxEncoder()(...) call internally awaits fetchContext, which POSTs
  // getChainInfo + getForgeState to beta — ~1-3s on warm isolates, more on
  // cold. When `genRequestedClaims` is called by the merchant and triggers
  // the encoder, merchant's did-connect-js aborts after 8s. Firing the fetch
  // here at init (module load) lets the cache populate while the worker sets
  // up the rest of the request pipeline.
  fetchContext(chainHost).catch((err) => {
    // Non-fatal: on failure the first real request will retry via the
    // encoder's own fetchContext call.
    console.warn('[initAuth] pre-warm chainInfo failed:', err?.message);
  });

  const authenticator = new WalletAuthenticator({
    wallet: wallet.toJSON(),
    appInfo: ({ baseUrl }: { baseUrl: string }) => ({
      name: 'Payment Kit',
      description: 'Payment Kit on Cloudflare Workers',
      icon: 'https://www.arcblock.io/favicon.ico',
      link: baseUrl,
    }),
    chainInfo: {
      type: 'arcblock',
      host: chainHost,
      id: 'beta',
    },
    txEncoder: createTxEncoder(),
    // CF Workers: chain RPC to beta.abtnetwork.io + getContext warm-up can exceed
    // the default 8s. Extend to 30s to match Node.js payment-kit behavior.
    timeout: 30000,
  });

  // did-connect-js 4.0.0 natively detects Hono apps via isHonoApp()
  const handlers = new WalletHandlers({ tokenStorage, authenticator });

  return { wallet, authenticator, handlers, tokenStorage };
}

/**
 * Attach DID Connect routes to a Hono app.
 *
 * Registers the login action with full profile handling, and all payment-related
 * DID Connect actions using the original handler implementations from
 * api/src/routes/connect/*.ts. The handlers execute real on-chain operations
 * (transfers, delegations, staking) via @ocap/client HTTP RPC calls, which
 * work in CF Workers. If a handler fails to attach, it falls back to a stub.
 *
 * Payment actions registered:
 *   collect, collect-batch, payment, subscription, setup,
 *   change-payment, change-plan, change-payer,
 *   recharge, recharge-account,
 *   delegation, overdraft-protection, re-stake,
 *   auto-recharge-auth
 */
export function attachDIDConnectRoutes(app: any, kv: KVNamespace, appSK: string) {
  const { handlers, tokenStorage } = initAuth(kv, appSK);

  // ---- Login action (full implementation) ----
  handlers.attach({
    app,
    action: 'login',
    claims: {
      profile: () => ({
        description: 'Please provide your profile to login',
        fields: ['fullName', 'email', 'avatar'],
      }),
    },
    onAuth: async ({ userDid, userPk, claims, updateSession }: any) => {
      const claim = claims.find((x: any) => x.type === 'profile');
      const { type: _type, signature: _sig, ...rest } = claim || {};
      await updateSession({
        result: {
          ...rest,
          did: userDid,
          pk: userPk,
        },
      });
    },
  });

  // ---- Payment DID Connect actions (real implementations) ----
  // These use the original handlers from api/src/routes/connect/*.ts.
  // esbuild aliases handle all dependency replacements (@blocklet/sdk, sequelize, etc.)
  // so that @ocap/client HTTP-based RPC calls work in CF Workers.
  const paymentHandlerList = [
    collectHandlers,
    collectBatchHandlers,
    payHandlers,
    setupHandlers,
    subscribeHandlers,
    changePaymentHandlers,
    changePlanHandlers,
    changePayerHandlers,
    rechargeHandlers,
    rechargeAccountHandlers,
    delegationHandlers,
    overdraftProtectionHandlers,
    reStakeHandlers,
    autoRechargeAuthHandlers,
  ];

  for (const handler of paymentHandlerList) {
    try {
      handlers.attach({ app, ...handler });
      console.log(`[CF DID Connect] Attached real handler: ${handler.action}`);
    } catch (err: any) {
      console.error(`[CF DID Connect] Failed to attach handler ${handler.action}, using stub:`, err?.message);
      // Fallback to stub if real handler fails to attach
      handlers.attach({
        app,
        action: handler.action,
        claims: {
          profile: () => ({
            description: `Please connect your wallet to proceed with ${handler.action}`,
            fields: ['fullName', 'email'],
          }),
        },
        onAuth: async ({ userDid, userPk, updateSession }: any) => {
          await updateSession({
            result: { did: userDid, pk: userPk },
          });
        },
      });
    }
  }

  return { handlers, tokenStorage };
}
