/**
 * mountA2aRequestHandler — DefaultRequestHandler + jsonRpcHandler を /a2a にマウント。
 *
 * Plan 2A の base card route (`/.well-known/agent-card.json`) と
 * extended card route (`/a2a/agent-card/extended`) はそのまま残す。
 * SDK の DefaultRequestHandler が `agent/getAuthenticatedExtendedCard` RPC を別途提供するが、
 * Plan 2A の自前 REST route と競合しない（path が異なる）。
 * 両者は同じ `buildExtendedCard` + `computeEffectiveScope` を呼ぶため実装ドリフトは起きない。
 */
import express from 'express';
import type { Express } from 'express';
import { A2AError, DefaultRequestHandler, type ServerCallContext } from '@a2a-js/sdk/server';
import { jsonRpcHandler } from '@a2a-js/sdk/server/express';
import type Provider from 'oidc-provider';
import type { Repository } from '../../db/repository.js';
import { logger } from '../../logger.js';
import { SqliteA2aTaskStore } from './task-store.js';
import { MaestroA2aExecutor } from './executor.js';
import { buildBaseCard, buildExtendedCard } from './agent-card.js';
import { computeEffectiveScope } from './delegation.js';
import { makeA2aUserBuilder } from './user-builder.js';
import type { A2aPrincipal } from './token-auth.js';
import { A2aLimiter, resolveA2aLimits, type A2aLimits } from './limiter.js';

export interface MountA2aRequestHandlerDeps {
  provider: Provider;
  repo: Repository;
  baseUrl: string;
  issuer: string;
  version: string;
  limits?: Partial<A2aLimits>;
}

type CardDeps = { baseUrl: string; issuer: string; version: string };

/**
 * DefaultRequestHandler のサブクラス。
 * `tasks/get`・`tasks/resubscribe`・`tasks/cancel` の 3 RPC に認証ゲートを追加する。
 *
 * 委任を取り消されたクライアント（または未認証クライアント）は、
 * すでに知っているタスク ID を使ってもタスクの状態・成果物を読み取れない（fail-closed）。
 * context.user が存在しない・isAuthenticated !== true の場合は A2AError.invalidRequest を投げ、
 * JsonRpcTransportHandler がそれを -32600 の JSON-RPC エラーレスポンスに変換する。
 *
 * sendMessage / sendMessageStream はゲートしない：executor.execute() が
 * principal のチェックを行い、unauthorized ならタスクを failed 状態で終了させる（既存の保護）。
 *
 * Task 7: limiter が注入された場合、認証チェックの後に rate-limit と
 * resubscribe スロットキャップを適用する（auth → rate → slot の順序を保証）。
 */
export class AuthGatedRequestHandler extends DefaultRequestHandler {
  private limiter?: A2aLimiter;

  constructor(
    agentCard: ConstructorParameters<typeof DefaultRequestHandler>[0],
    taskStore: ConstructorParameters<typeof DefaultRequestHandler>[1],
    executor: ConstructorParameters<typeof DefaultRequestHandler>[2],
    extendedCardProvider?: ConstructorParameters<typeof DefaultRequestHandler>[6],
    limiter?: A2aLimiter,
  ) {
    super(agentCard, taskStore, executor, undefined, undefined, undefined, extendedCardProvider);
    this.limiter = limiter;
  }

  override async getTask(params: Parameters<DefaultRequestHandler['getTask']>[0], context?: ServerCallContext) {
    this.requireAuthenticated(context);
    const grantId = this.meteredGrantId(context);
    if (this.limiter && !this.limiter.tryConsumeRate(grantId!)) {
      throw A2AError.invalidRequest('rate limit exceeded');
    }
    return super.getTask(params, context);
  }

  override async cancelTask(params: Parameters<DefaultRequestHandler['cancelTask']>[0], context?: ServerCallContext) {
    this.requireAuthenticated(context);
    const grantId = this.meteredGrantId(context);
    if (this.limiter && !this.limiter.tryConsumeRate(grantId!)) {
      throw A2AError.invalidRequest('rate limit exceeded');
    }
    return super.cancelTask(params, context);
  }

  /**
   * 認証ゲートを同期的に実行してから super.resubscribe() を返す。
   * 同期 throw にすることで、JsonRpcTransportHandler がジェネレータを取得する前に
   * エラーを捕捉できる（SSE ヘッダ送信前に正規の JSON-RPC エラーレスポンスを返せる）。
   *
   * limiter 注入時:
   *   1. auth check (sync throw)
   *   2. rate check (sync throw)
   *   3. slot acquire (sync throw if cap reached)
   *   4. wrap inner generator — finally releases slot on any exit path
   */
  override resubscribe(params: Parameters<DefaultRequestHandler['resubscribe']>[0], context?: ServerCallContext) {
    // 1. Auth — must be first and synchronous
    this.requireAuthenticated(context);
    // Fail-closed: with a limiter active, an authenticated caller missing a grantId is denied.
    const grantId = this.meteredGrantId(context);

    // No limiter → unmetered (e.g. test without limiter / metering off)
    if (!this.limiter) {
      return super.resubscribe(params, context);
    }

    // 2. Rate limit (limiter present ⟹ grantId is defined, else meteredGrantId threw)
    if (!this.limiter.tryConsumeRate(grantId!)) {
      throw A2AError.invalidRequest('rate limit exceeded');
    }

    // 3. Slot cap — synchronous acquire before returning generator
    if (!this.limiter.tryAcquireResubscribe(grantId!)) {
      throw A2AError.invalidRequest('too many concurrent resubscribe streams');
    }

    // 4. Wrap inner generator with a real wall-clock race so the stream-duration cap
    //    fires even when the inner stream emits no events (quiet-stream problem).
    //    Release slot in finally on every exit path.
    const inner = super.resubscribe(params, context);
    const limiter = this.limiter;
    const maxMs = this.limiter.limits.maxStreamSeconds * 1000;
    async function* wrapped() {
      // Idempotent release: guards against the JS generator edge case where a consumer
      // calls .return() before the first .next() — in that case the generator body's
      // finally block does not execute. In practice the SDK SSE path always calls
      // .next() before .return(), so this window is unreachable in production.
      let released = false;
      function release() {
        if (!released) { released = true; limiter.releaseResubscribe(grantId!); }
      }
      const deadline = Date.now() + maxMs;
      const it = inner[Symbol.asyncIterator]();
      try {
        while (true) {
          const remaining = deadline - Date.now();
          // Fire-and-forget the inner .return(): a `.return()` issued while a prior
          // `.next()` is still pending is queued behind it and does not settle until
          // that next() settles — which never happens for a quiet stream (the exact
          // case this cap targets). Awaiting it here would suspend `wrapped()` forever,
          // skip the `finally` below, and leak the resubscribe slot. Instead we drop the
          // slot immediately via `return` (→ finally → release) and let the inner
          // generator wind down on its own.
          if (remaining <= 0) { void it.return?.(undefined)?.catch(() => {}); return; }
          let timer: ReturnType<typeof setTimeout> | undefined;
          const timeout = new Promise<'timeout'>(res => {
            timer = setTimeout(() => res('timeout'), remaining);
          });
          const step = await Promise.race([it.next(), timeout]).finally(() => {
            if (timer !== undefined) clearTimeout(timer);
          });
          if (step === 'timeout') { void it.return?.(undefined)?.catch(() => {}); return; }
          if (step.done) return;
          yield step.value;
        }
      } finally {
        release();
      }
    }
    return wrapped() as ReturnType<DefaultRequestHandler['resubscribe']>;
  }

  private extractGrantId(context?: ServerCallContext): string | undefined {
    const p = (context?.user as any)?.a2aPrincipal as A2aPrincipal | undefined;
    return p?.grantId;
  }

  /**
   * Resolve the grantId for metering. When a limiter is active (a2a metering enabled),
   * an authenticated caller MUST carry a grantId — a missing one is treated as deny
   * (fail-closed), never as "skip the limits". Without a limiter (tests / metering off)
   * the caller is simply unmetered and undefined is returned.
   */
  private meteredGrantId(context?: ServerCallContext): string | undefined {
    const grantId = this.extractGrantId(context);
    if (this.limiter && !grantId) {
      throw A2AError.invalidRequest('unauthorized: missing delegation grant');
    }
    return grantId;
  }

  private requireAuthenticated(context?: ServerCallContext): void {
    const user = context?.user;
    if (!user || user.isAuthenticated !== true) {
      throw A2AError.invalidRequest(
        'unauthorized: caller not authenticated or delegation has been revoked',
      );
    }
  }
}

/**
 * Extended card provider（DefaultRequestHandler 7th 引数）。
 * ServerCallContext.user.a2aPrincipal が存在すれば scope 計算後の extended card を返す。
 * principal がない（未認証）または scope が null（unknown/inactive user）の場合は
 * base card を返す（スコープ情報の漏洩を防ぐ fail-closed 設計）。
 *
 * テスト可能にするため named export にする。
 */
export function makeExtendedCardProvider(
  repo: Repository,
  baseCard: ReturnType<typeof buildBaseCard>,
  deps: CardDeps,
) {
  return async (context?: ServerCallContext) => {
    const principal = (context?.user as any)?.a2aPrincipal as A2aPrincipal | undefined;
    if (!principal) return baseCard; // 未認証 → base card（no scope leak）
    try {
      const scope = await computeEffectiveScope(repo, principal);
      if (!scope) return baseCard; // unknown/inactive user → base card
      return buildExtendedCard(principal, {
        baseUrl: deps.baseUrl,
        issuer: deps.issuer,
        version: deps.version,
        userVisibleSpaceIds: scope.userVisibleSpaceIds,
        allowlistsBySpace: scope.allowlistsBySpace,
      });
    } catch (err) {
      logger.warn(`[a2a] extended card provider failed: ${err}`);
      return baseCard;
    }
  };
}

/**
 * DefaultRequestHandler + jsonRpcHandler を Express app の /a2a にマウントする。
 * server.ts の `if (a2aCfg.enabled)` ブロック内から呼ぶこと。
 * /a2a は /api 配下でないため requireAuth ブランケットに掛からない。
 * 認証は makeA2aUserBuilder（UserBuilder）が担う。
 */
export function mountA2aRequestHandler(app: Express, deps: MountA2aRequestHandlerDeps): void {
  const limits = resolveA2aLimits(deps.limits);
  const limiter = new A2aLimiter(limits);
  const taskStore = new SqliteA2aTaskStore(deps.repo);
  const executor = new MaestroA2aExecutor(deps.repo, { limiter });
  const cardDeps: CardDeps = {
    baseUrl: deps.baseUrl,
    issuer: deps.issuer,
    version: deps.version,
  };
  const baseCard = buildBaseCard(cardDeps);
  const extendedCardProvider = makeExtendedCardProvider(deps.repo, baseCard, cardDeps);
  const handler = new AuthGatedRequestHandler(
    baseCard,
    taskStore,
    executor,
    extendedCardProvider,  // 4th arg: forwarded to super's 7th slot
    limiter,               // 5th arg: shared rate/stream limiter (Task 7)
  );
  const userBuilder = makeA2aUserBuilder(deps.provider, deps.repo);
  // body-size limit: must precede jsonRpcHandler so oversized bodies are rejected
  // before the SDK parser sees them. body-parser skips re-parsing when req.body is
  // already set, so the SDK's own express.json() inside jsonRpcHandler won't double-parse.
  app.use('/a2a', express.json({ limit: limits.maxPayloadBytes }));
  app.use('/a2a', jsonRpcHandler({ requestHandler: handler, userBuilder }));
}
