type AlertMailGraphInput = {
  recipients: string[];
  subject: string;
  html: string;
  attachments?: Array<{
    name: string;
    contentType: string;
    contentBytes: string;
    isInline?: boolean;
    contentId?: string;
  }>;
};

type GraphTokenResponse = {
  access_token?: string;
  expires_in?: number;
  error?: string;
  error_description?: string;
};

type CachedToken = {
  value: string;
  expiresAt: number;
};

const graphGlobal = globalThis as typeof globalThis & {
  __rhConnectGraphToken?: CachedToken;
};

function requiredSetting(name: string) {
  const value = process.env[name]?.trim();

  if (!value) {
    throw new Error(`${name}_MISSING`);
  }

  return value;
}

function isValidEmail(value: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

async function readGraphError(response: Response) {
  const body = await response.text();

  try {
    const parsed = JSON.parse(body) as {
      error?: { code?: string; message?: string };
    };

    return [parsed.error?.code, parsed.error?.message]
      .filter(Boolean)
      .join(": ");
  } catch {
    return body.slice(0, 1_000);
  }
}

async function getGraphAccessToken() {
  const cached = graphGlobal.__rhConnectGraphToken;

  if (cached && cached.expiresAt > Date.now() + 60_000) {
    return cached.value;
  }

  const tenantId = requiredSetting("ENTRA_TENANT_ID");
  const clientId = requiredSetting("ENTRA_CLIENT_ID");
  const clientSecret = requiredSetting("ENTRA_CLIENT_SECRET");
  const response = await fetch(
    `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0/token`,
    {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: clientId,
        client_secret: clientSecret,
        scope: "https://graph.microsoft.com/.default",
        grant_type: "client_credentials",
      }),
      signal: AbortSignal.timeout(15_000),
    },
  );
  const result = (await response.json()) as GraphTokenResponse;

  if (!response.ok || !result.access_token) {
    throw new Error(
      `ALERT_MAIL_GRAPH_TOKEN_FAILED:${result.error ?? response.status}:` +
        `${result.error_description ?? "Unknown error"}`,
    );
  }

  const expiresIn = Number.isFinite(result.expires_in)
    ? Number(result.expires_in)
    : 3_600;

  graphGlobal.__rhConnectGraphToken = {
    value: result.access_token,
    expiresAt: Date.now() + expiresIn * 1_000,
  };

  return result.access_token;
}

export async function sendAlertMailWithGraph(mail: AlertMailGraphInput) {
  const fromEmail = requiredSetting("ALERT_MAIL_FROM_EMAIL").toLowerCase();

  if (!isValidEmail(fromEmail)) {
    throw new Error("ALERT_MAIL_INVALID_FROM_EMAIL");
  }

  if (mail.recipients.length === 0) {
    throw new Error("ALERT_MAIL_NO_RECIPIENT");
  }

  const invalidRecipient = mail.recipients.find(
    (recipient) => !isValidEmail(recipient),
  );

  if (invalidRecipient) {
    throw new Error("ALERT_MAIL_INVALID_RECIPIENT");
  }

  const accessToken = await getGraphAccessToken();
  const response = await fetch(
    `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(fromEmail)}/sendMail`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        message: {
          subject: mail.subject,
          body: { contentType: "HTML", content: mail.html },
          toRecipients: mail.recipients.map((address) => ({
            emailAddress: { address },
          })),
          attachments: mail.attachments?.map((attachment) => ({
            "@odata.type": "#microsoft.graph.fileAttachment",
            name: attachment.name,
            contentType: attachment.contentType,
            contentBytes: attachment.contentBytes,
            isInline: attachment.isInline,
            contentId: attachment.contentId,
          })),
        },
        saveToSentItems: true,
      }),
      signal: AbortSignal.timeout(30_000),
    },
  );

  if (!response.ok) {
    const details = await readGraphError(response);
    throw new Error(
      `ALERT_MAIL_GRAPH_SEND_FAILED:${response.status}:${details}`,
    );
  }

  // Graph répond 202 sans identifiant de message. L'identifiant de requête
  // permet tout de même de rapprocher l'envoi des journaux Microsoft 365.
  return {
    messageId:
      response.headers.get("request-id") ??
      response.headers.get("client-request-id"),
  };
}
