/**
 * Microsoft 365 / Graph API E-Mail-Sender.
 *
 * Auth: OAuth2 Client Credentials Flow (machine-to-machine, kein Browser).
 * Endpoint: POST https://graph.microsoft.com/v1.0/users/{sender}/sendMail
 * Berechtigung: Mail.Send (Application Permission, Admin-Consent erforderlich)
 *
 * Konfiguration (in dieser Reihenfolge, jeweils fehlende Felder ergänzen):
 * 1) Env: `M365_TENANT_ID`, `M365_CLIENT_ID`, `M365_CLIENT_SECRET`, optional `M365_AUDIT_BCC`
 * 2) Verschlüsselt in Settings → Key `send_provider_config` (JSON, siehe `parseM365FromSendProviderJson`)
 *
 * Absender-Adresse: kommt vom **Workspace** (`sender_email` / `sender_name`) bzw. M365-Sender-Pool
 * (`m365_senders` über die Settings-API) — Graph sendet als diese Mailbox im Tenant.
 */

import { getSecret } from '@/core/settings';

interface M365SendProviderJson {
  kind?: 'm365';
  tenantId?: string;
  clientId?: string;
  clientSecret?: string;
  tenant_id?: string;
  client_id?: string;
  client_secret?: string;
  auditBcc?: string;
}

function parseM365FromSendProviderJson(raw: string): {
  tenantId: string;
  clientId: string;
  clientSecret: string;
  auditBcc?: string;
} | null {
  try {
    const j = JSON.parse(raw) as M365SendProviderJson;
    if (j.kind && j.kind !== 'm365') return null;
    const tenantId = (j.tenantId ?? j.tenant_id)?.trim();
    const clientId = (j.clientId ?? j.client_id)?.trim();
    const clientSecret = (j.clientSecret ?? j.client_secret)?.trim();
    if (!tenantId || !clientId || !clientSecret) return null;
    return {
      tenantId,
      clientId,
      clientSecret,
      auditBcc: j.auditBcc?.trim() || undefined,
    };
  } catch {
    return null;
  }
}

function readM365FromSettings(): {
  tenantId: string;
  clientId: string;
  clientSecret: string;
  auditBcc?: string;
} | null {
  try {
    const raw = getSecret('send_provider_config');
    if (!raw) return null;
    return parseM365FromSendProviderJson(raw);
  } catch {
    return null;
  }
}

/** Env überschreibt einzelne Felder aus Settings (Docker / Secrets-Mount). */
export function resolveM365Credentials(): {
  tenantId: string;
  clientId: string;
  clientSecret: string;
  auditBcc?: string;
} | null {
  const fromDb = readM365FromSettings();
  const tenantId = process.env.M365_TENANT_ID?.trim() || fromDb?.tenantId;
  const clientId = process.env.M365_CLIENT_ID?.trim() || fromDb?.clientId;
  const clientSecret = process.env.M365_CLIENT_SECRET?.trim() || fromDb?.clientSecret;
  if (!tenantId || !clientId || !clientSecret) return null;
  const auditBcc = process.env.M365_AUDIT_BCC?.trim() || fromDb?.auditBcc;
  return { tenantId, clientId, clientSecret, auditBcc };
}

let cachedToken: string | null = null;
let tokenExpiresAt = 0;
let tokenCacheFingerprint = '';

/**
 * Sender-Resolution für M365.
 *
 * In der Plugin-Welt liefert der Caller (Worker, send-test, m365-Adapter) IMMER
 * eine explizite `from`-Adresse aus `workspace.senderEmail` — der frühere
 * COMPANY_SENDER_MAP-Fallback ist tot. Falls dennoch jemand `sendMail()` ohne
 * `from` aufruft, wirft der Helper unten mit einer klaren Meldung. Bei Bedarf
 * kann man einen Workspace-Lookup via `appSettings.m365_senders` rein-bauen,
 * aber der saubere Pfad ist immer "from explizit setzen".
 */

/**
 * Klassifizierter M365-Fehler. `kind` trennt Netzwerk-Probleme (kein HTTP-Status erreicht)
 * von Auth-Problemen (Token-Endpoint lieferte 4xx/5xx) und Graph-Problemen (sendMail-Endpoint
 * lieferte 4xx/5xx). `endpoint` ist die Host-Komponente, damit die UI sauber sagen kann
 * "Microsoft Login nicht erreichbar" vs. "Graph sendMail abgelehnt".
 */
export type M365ErrorKind = 'network' | 'auth' | 'graph' | 'config';
export class M365Error extends Error {
  readonly kind: M365ErrorKind;
  readonly endpoint: string;
  readonly httpStatus?: number;
  readonly bodySnippet?: string;
  constructor(opts: { kind: M365ErrorKind; endpoint: string; message: string; httpStatus?: number; bodySnippet?: string; cause?: unknown }) {
    super(opts.message, opts.cause ? { cause: opts.cause } : undefined);
    this.name = 'M365Error';
    this.kind = opts.kind;
    this.endpoint = opts.endpoint;
    this.httpStatus = opts.httpStatus;
    this.bodySnippet = opts.bodySnippet;
  }
}

export async function getAccessToken(): Promise<string> {
  const cred = resolveM365Credentials();
  if (!cred) {
    throw new M365Error({
      kind: 'config',
      endpoint: 'env',
      message:
        'Microsoft Graph nicht konfiguriert: setze M365_TENANT_ID, M365_CLIENT_ID und M365_CLIENT_SECRET (Umgebung) oder in /settings unter „Send Provider Config“ ein JSON mit kind \"m365\" (tenantId, clientId, clientSecret).',
    });
  }
  const fp = `${cred.tenantId}:${cred.clientId}`;
  if (fp !== tokenCacheFingerprint) {
    cachedToken = null;
    tokenCacheFingerprint = fp;
  }
  const nowSec = Math.floor(Date.now() / 1000);
  if (cachedToken && nowSec < tokenExpiresAt - 300) return cachedToken;

  const params = new URLSearchParams({
    client_id: cred.clientId,
    client_secret: cred.clientSecret,
    scope: 'https://graph.microsoft.com/.default',
    grant_type: 'client_credentials',
  });
  const tokenUrl = `https://login.microsoftonline.com/${cred.tenantId}/oauth2/v2.0/token`;
  let res: Response;
  try {
    res = await fetch(tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params.toString(),
    });
  } catch (err) {
    throw new M365Error({
      kind: 'network',
      endpoint: 'login.microsoftonline.com',
      message: `M365 Token unreachable: ${err instanceof Error ? err.message : String(err)}`,
      cause: err,
    });
  }
  if (!res.ok) {
    const txt = await res.text().catch(() => '');
    throw new M365Error({
      kind: 'auth',
      endpoint: 'login.microsoftonline.com',
      httpStatus: res.status,
      bodySnippet: txt.slice(0, 200),
      message: `M365 Token HTTP ${res.status}: ${txt.slice(0, 200)}`,
    });
  }
  const data = (await res.json()) as { access_token: string; expires_in: number };
  cachedToken = data.access_token;
  tokenExpiresAt = nowSec + (data.expires_in ?? 3600);
  return cachedToken;
}

export interface SendMailInput {
  /** Workspace-companyId (1=ITS, 2=KSM, 3=Spartarif). Bestimmt Default-Sender, falls `from` nicht gesetzt. */
  companyId: number;
  /**
   * Optional: expliziter Sender-Override (Email + Display-Name).
   * Wird gegenüber companyId-Mapping bevorzugt. Achtung: Email muss eine im Tenant
   * verfügbare Mailbox sein, sonst gibt Graph einen 404 zurück.
   */
  from?: { email: string; name: string };
  /** Empfänger — eine oder mehrere Email-Adressen */
  to: string[];
  /** Optional: zusätzliche CC-Empfänger */
  cc?: string[];
  /** Optional: zusätzliche BCC. Audit-BCC wird automatisch hinzugefügt falls konfiguriert. */
  bcc?: string[];
  /** Subject der Email */
  subject: string;
  /** HTML-Body der Email */
  bodyHtml: string;
  /** Optional: Plain-Text-Variante (falls leer, wird HTML genommen) */
  bodyText?: string;
  /** Optional: Reply-To setzen (falls abweichend von Sender) */
  replyTo?: string;
  /** In "Gesendete Elemente" der Sender-Inbox speichern (Default: true) */
  saveToSent?: boolean;
}

export interface SendMailResult {
  ok: true;
  sender: string;
  to: string[];
  bcc: string[];
  durationMs: number;
}

export async function sendMail(input: SendMailInput): Promise<SendMailResult> {
  const start = Date.now();
  // Sender-Resolution: input.from ist Pflicht im Plugin-Pfad — der Caller
  // (Worker, send-test, m365-Adapter) liefert workspace.senderEmail.
  if (!input.from?.email) {
    throw new M365Error({
      kind: 'config',
      endpoint: 'sendMail',
      message:
        'sendMail() ohne explicit `from` aufgerufen. Im Plugin-Build wird der Sender immer vom Caller gesetzt (workspace.senderEmail). companyId allein reicht nicht mehr.',
    });
  }
  const sender = { email: input.from.email, name: input.from.name };
  const token = await getAccessToken();

  // BCC: User-übergebene + Audit-BCC (falls gesetzt + nicht schon enthalten)
  const finalBcc = [...(input.bcc ?? [])];
  const audit = resolveM365Credentials()?.auditBcc;
  if (audit && !finalBcc.includes(audit) && !input.to.includes(audit)) {
    finalBcc.push(audit);
  }

  const message = {
    message: {
      subject: input.subject,
      body: {
        contentType: 'HTML' as const,
        content: input.bodyHtml,
      },
      toRecipients: input.to.map((email) => ({ emailAddress: { address: email } })),
      ccRecipients: (input.cc ?? []).map((email) => ({ emailAddress: { address: email } })),
      bccRecipients: finalBcc.map((email) => ({ emailAddress: { address: email } })),
      ...(input.replyTo ? { replyTo: [{ emailAddress: { address: input.replyTo } }] } : {}),
    },
    saveToSentItems: input.saveToSent ?? true,
  };

  const sendMailUrl = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(sender.email)}/sendMail`;
  let res: Response;
  try {
    res = await fetch(sendMailUrl, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(message),
    });
  } catch (err) {
    throw new M365Error({
      kind: 'network',
      endpoint: 'graph.microsoft.com',
      message: `Graph sendMail unreachable: ${err instanceof Error ? err.message : String(err)}`,
      cause: err,
    });
  }

  if (!res.ok) {
    const txt = await res.text().catch(() => '');
    throw new M365Error({
      kind: 'graph',
      endpoint: 'graph.microsoft.com',
      httpStatus: res.status,
      bodySnippet: txt.slice(0, 400),
      message: `Graph sendMail HTTP ${res.status}: ${txt.slice(0, 400)}`,
    });
  }

  return {
    ok: true,
    sender: sender.email,
    to: input.to,
    bcc: finalBcc,
    durationMs: Date.now() - start,
  };
}

/**
 * Read-only Health-Check gegen Graph: holt Sender-Mailbox via GET /users/{email}.
 * Verbraucht weder Token-Quota nennenswert noch sendet Mail. Wirft M365Error analog
 * zu sendMail, damit die Diagnose-Route den `kind` direkt durchreichen kann.
 */
export async function pingGraphUser(email: string): Promise<{ ok: true; durationMs: number; displayName?: string }> {
  const start = Date.now();
  const token = await getAccessToken();
  const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(email)}`;
  let res: Response;
  try {
    res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  } catch (err) {
    throw new M365Error({
      kind: 'network',
      endpoint: 'graph.microsoft.com',
      message: `Graph ping unreachable: ${err instanceof Error ? err.message : String(err)}`,
      cause: err,
    });
  }
  if (!res.ok) {
    const txt = await res.text().catch(() => '');
    throw new M365Error({
      kind: 'graph',
      endpoint: 'graph.microsoft.com',
      httpStatus: res.status,
      bodySnippet: txt.slice(0, 200),
      message: `Graph ping HTTP ${res.status}: ${txt.slice(0, 200)}`,
    });
  }
  const data = (await res.json().catch(() => ({}))) as { displayName?: string };
  return { ok: true, durationMs: Date.now() - start, displayName: data.displayName };
}

export function getAuditBcc(): string | undefined {
  return resolveM365Credentials()?.auditBcc;
}

export function isM365Configured(): boolean {
  return resolveM365Credentials() !== null;
}

/**
 * Kept as a no-op stub for legacy callers (Settings-UI dropdown). Returns
 * undefined unconditionally — workspaces deklarieren ihren Sender selbst.
 * Wird in einem späteren Cleanup-Pass entfernt, sobald die UI auf
 * getActiveM365Senders() umgestellt ist.
 */
export function getSenderForCompany(_companyId: number): { email: string; name: string } | undefined {
  return undefined;
}
