import { env } from "./env";
import { prisma } from "./prisma";
import { decryptSecret, encryptSecret } from "./tokenCrypto";

const AUTHORIZE_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
const TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
const GRAPH_BASE = "https://graph.microsoft.com/v1.0";
const SCOPES = "offline_access User.Read Mail.Read";

export type MicrosoftAppConfig = {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
};

/** The redirect URI is fixed by our own callback route, not admin-entered — this
 * is what gets registered in the Azure AD app registration. */
export function getDefaultRedirectUri(): string {
  return `${env.API_URL}/api/mail/callback`;
}

export async function getMicrosoftAppConfig(): Promise<MicrosoftAppConfig | null> {
  const settings = await prisma.integrationSettings.findUnique({ where: { id: "singleton" } });
  if (!settings?.microsoftClientId || !settings.microsoftClientSecret) {
    return null;
  }
  return {
    clientId: settings.microsoftClientId,
    clientSecret: decryptSecret(settings.microsoftClientSecret),
    redirectUri: getDefaultRedirectUri(),
  };
}

export async function saveMicrosoftAppConfig(config: { clientId: string; clientSecret: string }): Promise<void> {
  await prisma.integrationSettings.upsert({
    where: { id: "singleton" },
    update: {
      microsoftClientId: config.clientId,
      microsoftClientSecret: encryptSecret(config.clientSecret),
      microsoftRedirectUri: getDefaultRedirectUri(),
    },
    create: {
      id: "singleton",
      microsoftClientId: config.clientId,
      microsoftClientSecret: encryptSecret(config.clientSecret),
      microsoftRedirectUri: getDefaultRedirectUri(),
    },
  });
}

export function buildAuthorizeUrl(config: MicrosoftAppConfig, state: string): string {
  const params = new URLSearchParams({
    client_id: config.clientId,
    response_type: "code",
    redirect_uri: config.redirectUri,
    response_mode: "query",
    scope: SCOPES,
    state,
  });
  return `${AUTHORIZE_URL}?${params.toString()}`;
}

type TokenResponse = {
  access_token: string;
  refresh_token: string;
  expires_in: number;
};

async function requestToken(config: MicrosoftAppConfig, body: Record<string, string>): Promise<TokenResponse> {
  const res = await fetch(TOKEN_URL, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      client_id: config.clientId,
      client_secret: config.clientSecret,
      ...body,
    }),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Microsoft token request failed (${res.status}): ${text}`);
  }
  return res.json() as Promise<TokenResponse>;
}

export function exchangeCodeForTokens(config: MicrosoftAppConfig, code: string): Promise<TokenResponse> {
  return requestToken(config, {
    grant_type: "authorization_code",
    code,
    redirect_uri: config.redirectUri,
  });
}

export function refreshTokens(config: MicrosoftAppConfig, refreshToken: string): Promise<TokenResponse> {
  return requestToken(config, {
    grant_type: "refresh_token",
    refresh_token: refreshToken,
  });
}

export async function fetchGraphProfile(
  accessToken: string
): Promise<{ email: string; displayName: string }> {
  const res = await fetch(`${GRAPH_BASE}/me`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  if (!res.ok) throw new Error(`Failed to fetch Microsoft profile (${res.status})`);
  const data = (await res.json()) as { mail?: string; userPrincipalName: string; displayName: string };
  return { email: data.mail ?? data.userPrincipalName, displayName: data.displayName };
}

export type GraphMessage = {
  id: string;
  subject: string;
  bodyPreview: string;
  from?: { emailAddress?: { name?: string; address?: string } };
  isRead: boolean;
  flag?: { flagStatus?: "notFlagged" | "flagged" | "complete" };
  receivedDateTime: string;
  webLink?: string;
  "@removed"?: { reason: string };
};

/** Fetches inbox changes since the last sync via Graph's delta query — the first
 * call (no deltaLink) returns the current inbox state; subsequent calls with the
 * stored deltaLink return only what changed, which keeps polling cheap. */
export async function fetchInboxDelta(
  accessToken: string,
  deltaLink: string | null
): Promise<{ messages: GraphMessage[]; nextDeltaLink: string }> {
  const url =
    deltaLink ??
    `${GRAPH_BASE}/me/mailFolders/inbox/messages/delta?$select=subject,bodyPreview,from,isRead,flag,receivedDateTime,webLink`;

  const messages: GraphMessage[] = [];
  let nextUrl: string | null = url;
  let nextDeltaLink = deltaLink ?? "";

  while (nextUrl) {
    const res: Response = await fetch(nextUrl, {
      headers: { Authorization: `Bearer ${accessToken}`, Prefer: 'odata.maxpagesize=25' },
    });
    if (!res.ok) throw new Error(`Failed to fetch inbox delta (${res.status})`);
    const data = (await res.json()) as {
      value: GraphMessage[];
      "@odata.nextLink"?: string;
      "@odata.deltaLink"?: string;
    };
    messages.push(...data.value);
    nextUrl = data["@odata.nextLink"] ?? null;
    if (data["@odata.deltaLink"]) nextDeltaLink = data["@odata.deltaLink"];
  }

  return { messages, nextDeltaLink };
}
