import { formatUnknownError } from "./errors.js";
import type { MSTeamsAdapter } from "./messenger.js";
import type { MSTeamsCredentials } from "./token.js";
import { buildUserAgent } from "./user-agent.js";

/**
 * Resolved Teams SDK modules loaded lazily to avoid importing when the
 * provider is disabled.
 */
export type MSTeamsTeamsSdk = {
  App: typeof import("@microsoft/teams.apps").App;
  Client: typeof import("@microsoft/teams.api").Client;
};

/**
 * A Teams SDK App instance used for token management and proactive messaging.
 */
export type MSTeamsApp = InstanceType<MSTeamsTeamsSdk["App"]>;

/**
 * Token provider compatible with the existing codebase, wrapping the Teams
 * SDK App's token methods.
 */
export type MSTeamsTokenProvider = {
  getAccessToken: (scope: string) => Promise<string>;
};

type MSTeamsBotIdentity = {
  id?: string;
  name?: string;
};

type MSTeamsSendContext = {
  sendActivity: (textOrActivity: string | object) => Promise<unknown>;
  updateActivity: (activityUpdate: object) => Promise<{ id?: string } | void>;
  deleteActivity: (activityId: string) => Promise<void>;
};

type MSTeamsProcessContext = MSTeamsSendContext & {
  activity: Record<string, unknown> | undefined;
  sendActivities: (
    activities: Array<{ type: string } & Record<string, unknown>>,
  ) => Promise<unknown[]>;
};

export async function loadMSTeamsSdk(): Promise<MSTeamsTeamsSdk> {
  const [appsModule, apiModule] = await Promise.all([
    import("@microsoft/teams.apps"),
    import("@microsoft/teams.api"),
  ]);
  return {
    App: appsModule.App,
    Client: apiModule.Client,
  };
}

/**
 * Create a lightweight no-op HTTP plugin stub that satisfies the Teams SDK's
 * plugin discovery by name ("http") but does NOT spin up an Express server.
 *
 * The default HttpPlugin in @microsoft/teams.apps registers an Express
 * middleware with the pattern `/api*`.  When the host application (OpenClaw)
 * uses Express 5 — which depends on path-to-regexp v8 — that pattern is
 * invalid and throws:
 *
 *   Missing parameter name at index 5: /api*
 *
 * OpenClaw manages its own Express server for the Teams webhook endpoint, so
 * the SDK's built-in HTTP server is unnecessary.  Passing this stub prevents
 * the SDK from creating the default HttpPlugin and avoids the crash.
 *
 * See: https://github.com/openclaw/openclaw/issues/55161
 */
async function createNoOpHttpPlugin(): Promise<unknown> {
  // Lazy-import reflect-metadata (required by the Teams SDK decorator system)
  // and the decorator key constants so we can tag the stub class correctly.
  //
  // FRAGILE: these are internal SDK paths (not public API).  If
  // @microsoft/teams.apps changes its dist layout, these imports will break.
  // Pin the SDK version and re-verify after any upgrade.
  await import("reflect-metadata");
  const { PLUGIN_METADATA_KEY } =
    await import("@microsoft/teams.apps/dist/types/plugin/decorators/plugin.js");
  const { PLUGIN_DEPENDENCIES_METADATA_KEY } =
    await import("@microsoft/teams.apps/dist/types/plugin/decorators/dependency.js");
  const { PLUGIN_EVENTS_METADATA_KEY } =
    await import("@microsoft/teams.apps/dist/types/plugin/decorators/event.js");

  class NoOpHttpPlugin {
    onInit() {}
    async onStart() {}
    onStop() {}
    asServer() {
      return {
        onRequest: undefined as unknown,
        initialize: async () => {},
        start: async () => {},
        stop: async () => {},
      } as {
        onRequest: unknown;
        initialize: (opts?: unknown) => Promise<void>;
        start: (port?: number | string) => Promise<void>;
        stop: () => Promise<void>;
      };
    }
  }

  Reflect.defineMetadata(
    PLUGIN_METADATA_KEY,
    { name: "http", version: "0.0.0", description: "no-op stub (express 5 compat)" },
    NoOpHttpPlugin,
  );
  Reflect.defineMetadata(PLUGIN_DEPENDENCIES_METADATA_KEY, [], NoOpHttpPlugin);
  Reflect.defineMetadata(PLUGIN_EVENTS_METADATA_KEY, [], NoOpHttpPlugin);

  return new NoOpHttpPlugin();
}

/**
 * Create a Teams SDK App instance from credentials. The App manages token
 * acquisition, JWT validation, and the HTTP server lifecycle.
 *
 * This replaces the previous CloudAdapter + MsalTokenProvider + authorizeJWT
 * from @microsoft/agents-hosting.
 */
export async function createMSTeamsApp(
  creds: MSTeamsCredentials,
  sdk: MSTeamsTeamsSdk,
): Promise<MSTeamsApp> {
  const noOpHttp = await createNoOpHttpPlugin();
  // Use type assertion: the SDK's AppOptions generic narrows `plugins` to
  // Array<TPlugin>, but our no-op stub satisfies the runtime contract without
  // matching the decorator-heavy IPlugin type at compile time.
  return new sdk.App({
    clientId: creds.appId,
    clientSecret: creds.appPassword,
    tenantId: creds.tenantId,
    plugins: [noOpHttp],
  } as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
}

/**
 * Build a token provider that uses the Teams SDK App for token acquisition.
 */
export function createMSTeamsTokenProvider(app: MSTeamsApp): MSTeamsTokenProvider {
  return {
    async getAccessToken(scope: string): Promise<string> {
      if (scope.includes("graph.microsoft.com")) {
        const token = await (
          app as unknown as { getAppGraphToken(): Promise<{ toString(): string } | null> }
        ).getAppGraphToken();
        return token ? String(token) : "";
      }
      const token = await (
        app as unknown as { getBotToken(): Promise<{ toString(): string } | null> }
      ).getBotToken();
      return token ? String(token) : "";
    },
  };
}

function createBotTokenGetter(app: MSTeamsApp): () => Promise<string | undefined> {
  return async () => {
    const token = await (
      app as unknown as { getBotToken(): Promise<{ toString(): string } | null> }
    ).getBotToken();
    return token ? String(token) : undefined;
  };
}

function createApiClient(
  sdk: MSTeamsTeamsSdk,
  serviceUrl: string,
  getToken: () => Promise<string | undefined>,
) {
  return new sdk.Client(serviceUrl, {
    token: async () => (await getToken()) || undefined,
    headers: { "User-Agent": buildUserAgent() },
  } as Record<string, unknown>);
}

function normalizeOutboundActivity(textOrActivity: string | object): Record<string, unknown> {
  return typeof textOrActivity === "string"
    ? ({ type: "message", text: textOrActivity } as Record<string, unknown>)
    : (textOrActivity as Record<string, unknown>);
}

function createSendContext(params: {
  sdk: MSTeamsTeamsSdk;
  serviceUrl?: string;
  conversationId?: string;
  conversationType?: string;
  bot?: MSTeamsBotIdentity;
  replyToActivityId?: string;
  getToken: () => Promise<string | undefined>;
  treatInvokeResponseAsNoop?: boolean;
}): MSTeamsSendContext {
  const apiClient =
    params.serviceUrl && params.conversationId
      ? createApiClient(params.sdk, params.serviceUrl, params.getToken)
      : undefined;

  return {
    async sendActivity(textOrActivity: string | object): Promise<unknown> {
      const msg = normalizeOutboundActivity(textOrActivity);
      if (params.treatInvokeResponseAsNoop && msg.type === "invokeResponse") {
        return { id: "invokeResponse" };
      }
      if (!apiClient || !params.conversationId) {
        return { id: "unknown" };
      }

      return await apiClient.conversations.activities(params.conversationId).create({
        type: "message",
        ...msg,
        from: params.bot?.id
          ? { id: params.bot.id, name: params.bot.name ?? "", role: "bot" }
          : undefined,
        conversation: {
          id: params.conversationId,
          conversationType: params.conversationType ?? "personal",
        },
        ...(params.replyToActivityId && !msg.replyToId
          ? { replyToId: params.replyToActivityId }
          : {}),
      } as Parameters<
        typeof apiClient.conversations.activities extends (id: string) => {
          create: (a: infer T) => unknown;
        }
          ? never
          : never
      >[0]);
    },

    async updateActivity(activityUpdate: object): Promise<{ id?: string } | void> {
      const nextActivity = activityUpdate as { id?: string } & Record<string, unknown>;
      const activityId = nextActivity.id;
      if (!activityId) {
        throw new Error("updateActivity requires an activity id");
      }
      if (!params.serviceUrl || !params.conversationId) {
        return { id: "unknown" };
      }
      return await updateActivityViaRest({
        serviceUrl: params.serviceUrl,
        conversationId: params.conversationId,
        activityId,
        activity: nextActivity,
        token: await params.getToken(),
      });
    },

    async deleteActivity(activityId: string): Promise<void> {
      if (!activityId) {
        throw new Error("deleteActivity requires an activity id");
      }
      if (!params.serviceUrl || !params.conversationId) {
        return;
      }
      await deleteActivityViaRest({
        serviceUrl: params.serviceUrl,
        conversationId: params.conversationId,
        activityId,
        token: await params.getToken(),
      });
    },
  };
}

function createProcessContext(params: {
  sdk: MSTeamsTeamsSdk;
  activity: Record<string, unknown> | undefined;
  getToken: () => Promise<string | undefined>;
}): MSTeamsProcessContext {
  const serviceUrl = params.activity?.serviceUrl as string | undefined;
  const conversationId = (params.activity?.conversation as Record<string, unknown>)?.id as
    | string
    | undefined;
  const conversationType = (params.activity?.conversation as Record<string, unknown>)
    ?.conversationType as string | undefined;
  const replyToActivityId = params.activity?.id as string | undefined;
  const bot: MSTeamsBotIdentity | undefined =
    params.activity?.recipient && typeof params.activity.recipient === "object"
      ? {
          id: (params.activity.recipient as Record<string, unknown>).id as string | undefined,
          name: (params.activity.recipient as Record<string, unknown>).name as string | undefined,
        }
      : undefined;
  const sendContext = createSendContext({
    sdk: params.sdk,
    serviceUrl,
    conversationId,
    conversationType,
    bot,
    replyToActivityId,
    getToken: params.getToken,
    treatInvokeResponseAsNoop: true,
  });

  return {
    activity: params.activity,
    ...sendContext,
    async sendActivities(activities: Array<{ type: string } & Record<string, unknown>>) {
      const results = [];
      for (const activity of activities) {
        results.push(await sendContext.sendActivity(activity));
      }
      return results;
    },
  };
}

/**
 * Update an existing activity via the Bot Framework REST API.
 * PUT /v3/conversations/{conversationId}/activities/{activityId}
 */
async function updateActivityViaRest(params: {
  serviceUrl: string;
  conversationId: string;
  activityId: string;
  activity: Record<string, unknown>;
  token?: string;
}): Promise<{ id?: string }> {
  const { serviceUrl, conversationId, activityId, activity, token } = params;
  const baseUrl = serviceUrl.replace(/\/+$/, "");
  const url = `${baseUrl}/v3/conversations/${encodeURIComponent(conversationId)}/activities/${encodeURIComponent(activityId)}`;

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    "User-Agent": buildUserAgent(),
  };
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }

  const response = await fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify({
      type: "message",
      ...activity,
      id: activityId,
    }),
  });

  if (!response.ok) {
    const body = await response.text().catch(() => "");
    throw Object.assign(new Error(`updateActivity failed: HTTP ${response.status} ${body}`), {
      statusCode: response.status,
    });
  }

  return await response.json().catch(() => ({ id: activityId }));
}

/**
 * Delete an existing activity via the Bot Framework REST API.
 * DELETE /v3/conversations/{conversationId}/activities/{activityId}
 */
async function deleteActivityViaRest(params: {
  serviceUrl: string;
  conversationId: string;
  activityId: string;
  token?: string;
}): Promise<void> {
  const { serviceUrl, conversationId, activityId, token } = params;
  const baseUrl = serviceUrl.replace(/\/+$/, "");
  const url = `${baseUrl}/v3/conversations/${encodeURIComponent(conversationId)}/activities/${encodeURIComponent(activityId)}`;

  const headers: Record<string, string> = {
    "User-Agent": buildUserAgent(),
  };
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }

  const response = await fetch(url, {
    method: "DELETE",
    headers,
  });

  if (!response.ok) {
    const body = await response.text().catch(() => "");
    throw Object.assign(new Error(`deleteActivity failed: HTTP ${response.status} ${body}`), {
      statusCode: response.status,
    });
  }
}

/**
 * Build a CloudAdapter-compatible adapter using the Teams SDK REST client.
 *
 * This replaces the previous CloudAdapter from @microsoft/agents-hosting.
 * For incoming requests: the App's HttpPlugin handles JWT validation.
 * For proactive sends: uses the Bot Framework REST API via
 * @microsoft/teams.api Client.
 */
export function createMSTeamsAdapter(app: MSTeamsApp, sdk: MSTeamsTeamsSdk): MSTeamsAdapter {
  return {
    async continueConversation(_appId, reference, logic) {
      const serviceUrl = reference.serviceUrl;
      if (!serviceUrl) {
        throw new Error("Missing serviceUrl in conversation reference");
      }

      const conversationId = reference.conversation?.id;
      if (!conversationId) {
        throw new Error("Missing conversation.id in conversation reference");
      }

      const sendContext = createSendContext({
        sdk,
        serviceUrl,
        conversationId,
        conversationType: reference.conversation?.conversationType,
        bot: reference.agent ?? undefined,
        getToken: createBotTokenGetter(app),
      });

      await logic(sendContext);
    },

    async process(req, res, logic) {
      const request = req as { body?: Record<string, unknown> };
      const response = res as {
        status: (code: number) => { send: (body?: unknown) => void };
      };

      const activity = request.body;
      const isInvoke = (activity as Record<string, unknown>)?.type === "invoke";

      try {
        const context = createProcessContext({
          sdk,
          activity,
          getToken: createBotTokenGetter(app),
        });

        // For invoke activities, send HTTP 200 immediately before running
        // handler logic so slow operations (file uploads, reflections) don't
        // hit Teams invoke timeouts ("unable to reach app").
        if (isInvoke) {
          response.status(200).send();
        }

        await logic(context);

        if (!isInvoke) {
          response.status(200).send();
        }
      } catch (err) {
        if (!isInvoke) {
          response.status(500).send({ error: formatUnknownError(err) });
        }
      }
    },

    async updateActivity(_context, activity) {
      // No-op: updateActivity is handled via REST in streaming-message.ts
    },

    async deleteActivity(_context, _reference) {
      // No-op: deleteActivity not yet implemented for Teams SDK adapter
    },
  };
}

export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
  const sdk = await loadMSTeamsSdk();
  const app = await createMSTeamsApp(creds, sdk);
  return { sdk, app };
}

/**
 * Create a Bot Framework JWT validator with strict multi-issuer support.
 *
 * During Microsoft's transition, inbound service tokens can be signed by either:
 * - Legacy Bot Framework issuer/JWKS
 * - Entra issuer/JWKS
 *
 * Security invariants are preserved for both paths:
 * - signature verification (issuer-specific JWKS)
 * - audience validation (appId)
 * - issuer validation (strict allowlist)
 * - expiration validation (Teams SDK defaults)
 */
export async function createBotFrameworkJwtValidator(creds: MSTeamsCredentials): Promise<{
  validate: (authHeader: string, serviceUrl?: string) => Promise<boolean>;
}> {
  const { JwtValidator } =
    await import("@microsoft/teams.apps/dist/middleware/auth/jwt-validator.js");

  const botFrameworkValidator = new JwtValidator({
    clientId: creds.appId,
    tenantId: creds.tenantId,
    validateIssuer: { allowedIssuer: "https://api.botframework.com" },
    jwksUriOptions: {
      type: "uri",
      uri: "https://login.botframework.com/v1/.well-known/keys",
    },
  });

  const entraValidator = new JwtValidator({
    clientId: creds.appId,
    tenantId: creds.tenantId,
    validateIssuer: { allowedTenantIds: [creds.tenantId] },
    jwksUriOptions: {
      type: "uri",
      uri: "https://login.microsoftonline.com/common/discovery/v2.0/keys",
    },
  });

  async function validateWithFallback(
    token: string,
    overrides: { validateServiceUrl: { expectedServiceUrl: string } } | undefined,
  ): Promise<boolean> {
    for (const validator of [botFrameworkValidator, entraValidator]) {
      try {
        const result = await validator.validateAccessToken(token, overrides);
        if (result != null) {
          return true;
        }
      } catch {
        continue;
      }
    }
    return false;
  }

  return {
    async validate(authHeader: string, serviceUrl?: string): Promise<boolean> {
      const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
      if (!token) {
        return false;
      }

      const overrides = serviceUrl
        ? ({ validateServiceUrl: { expectedServiceUrl: serviceUrl } } as const)
        : undefined;
      return await validateWithFallback(token, overrides);
    },
  };
}
