import {
  Injectable,
  Logger,
  UnauthorizedException,
  ServiceUnavailableException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { RedisService } from '../redis/redis.service';

export interface M365Email {
  id: string;
  subject: string;
  bodyPreview: string;
  receivedDateTime: string;
  from: { emailAddress: { name: string; address: string } };
  hasAttachments: boolean;
  isRead: boolean;
  webLink: string;
}

export interface M365CalendarEvent {
  id: string;
  subject: string;
  start: { dateTime: string; timeZone: string };
  end: { dateTime: string; timeZone: string };
  location: { displayName: string };
  organizer: { emailAddress: { name: string; address: string } };
  attendees: Array<{ emailAddress: { name: string; address: string }; type: string }>;
  isOnlineMeeting: boolean;
  onlineMeetingUrl: string | null;
  webLink: string;
}

export interface M365Task {
  id: string;
  title: string;
  status: string;
  importance: string;
  dueDateTime: { dateTime: string; timeZone: string } | null;
  completedDateTime: { dateTime: string; timeZone: string } | null;
  createdDateTime: string;
}

export interface M365TaskList {
  id: string;
  displayName: string;
  tasks: M365Task[];
}

/** Flache Aufgabe inkl. Body-Inhalt (für Dashboard Aufgaben-Tab) */
export interface M365TaskFlat {
  id: string;
  listId: string;
  listName: string;
  title: string;
  status: string;
  importance: string;
  dueDateTime: { dateTime: string; timeZone: string } | null;
  bodyContent: string | null; // Enthält "[INSIGHT_CRM:{activityId}]" wenn synchronisiert
  createdDateTime: string;
}

export interface M365Contact {
  id: string;
  displayName: string;
  emailAddresses: Array<{ name: string; address: string }>;
  mobilePhone: string | null;
  businessPhones: string[];
  jobTitle: string | null;
  companyName: string | null;
}

export interface M365UserProfile {
  givenName: string | null;
  surname: string | null;
  displayName: string | null;
  mobilePhone: string | null;
  businessPhones: string[];
  city: string | null;
  streetAddress: string | null;
  postalCode: string | null;
  jobTitle: string | null;
  department: string | null;
  companyName: string | null;
  officeLocation: string | null;
}

export interface M365MailFolder {
  id: string;
  displayName: string;
  totalItemCount: number;
  unreadItemCount: number;
  childFolderCount: number;
  wellKnownName: string | null;
}

const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
const CACHE_TTL = 300; // 5 Minuten

@Injectable()
export class GraphService {
  private readonly logger = new Logger(GraphService.name);
  private readonly coreServiceUrl: string;

  constructor(
    private readonly config: ConfigService,
    private readonly redis: RedisService,
  ) {
    this.coreServiceUrl =
      this.config.get<string>('CORE_SERVICE_URL') ?? 'http://core:3000';
  }

  // ── Token vom Core-Service holen ──────────────────────────────────────

  async getM365Token(userJwt: string): Promise<string> {
    const url = `${this.coreServiceUrl}/api/v1/users/me/integrations/microsoft-365/token`;

    let resp: Response;
    try {
      resp = await fetch(url, {
        headers: { Authorization: `Bearer ${userJwt}` },
        signal: AbortSignal.timeout(5000),
      });
    } catch (err) {
      throw new ServiceUnavailableException(
        `Core-Service nicht erreichbar: ${(err as Error).message}`,
      );
    }

    if (resp.status === 404) {
      throw new UnauthorizedException(
        'Keine Microsoft 365 Verbindung vorhanden — bitte zuerst verbinden',
      );
    }

    if (!resp.ok) {
      throw new ServiceUnavailableException(
        `Core-Service Fehler: ${resp.status} ${resp.statusText}`,
      );
    }

    const body = (await resp.json()) as {
      success: boolean;
      data: { accessToken: string };
    };

    if (!body.success || !body.data?.accessToken) {
      throw new ServiceUnavailableException('Kein M365-Token erhalten');
    }

    return body.data.accessToken;
  }

  // ── Graph API Helpers ─────────────────────────────────────────────────

  private async graphPost<T>(
    accessToken: string,
    path: string,
    body: unknown,
  ): Promise<T> {
    const resp = await fetch(`${GRAPH_BASE}${path}`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(10000),
    });

    if (!resp.ok) {
      const errBody = (await resp.json().catch(() => ({}))) as {
        error?: { message?: string };
      };
      this.logger.error(
        `Graph API POST Fehler ${resp.status} auf ${path}: ${errBody.error?.message ?? resp.statusText}`,
      );
      throw new ServiceUnavailableException(
        `Graph API Fehler ${resp.status}: ${errBody.error?.message ?? resp.statusText}`,
      );
    }

    return resp.json() as Promise<T>;
  }

  private async graphPatch<T>(
    accessToken: string,
    path: string,
    body: unknown,
  ): Promise<T> {
    const resp = await fetch(`${GRAPH_BASE}${path}`, {
      method: 'PATCH',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(10000),
    });

    if (!resp.ok) {
      const errBody = (await resp.json().catch(() => ({}))) as {
        error?: { message?: string };
      };
      this.logger.error(
        `Graph API PATCH Fehler ${resp.status} auf ${path}: ${errBody.error?.message ?? resp.statusText}`,
      );
      throw new ServiceUnavailableException(
        `Graph API Fehler ${resp.status}: ${errBody.error?.message ?? resp.statusText}`,
      );
    }

    // PATCH kann 204 No Content zurückgeben
    if (resp.status === 204) return {} as T;
    return resp.json() as Promise<T>;
  }

  private async graphGet<T>(
    accessToken: string,
    path: string,
    params?: Record<string, string>,
  ): Promise<T> {
    const url = new URL(`${GRAPH_BASE}${path}`);
    if (params) {
      for (const [k, v] of Object.entries(params)) {
        url.searchParams.set(k, v);
      }
    }

    const resp = await fetch(url.toString(), {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        Accept: 'application/json',
      },
      signal: AbortSignal.timeout(10000),
    });

    if (!resp.ok) {
      const errBody = (await resp.json().catch(() => ({}))) as {
        error?: { message?: string };
      };
      this.logger.error(
        `Graph API Fehler ${resp.status} auf ${path}: ${errBody.error?.message ?? resp.statusText}`,
      );
      throw new ServiceUnavailableException(
        `Graph API Fehler ${resp.status}: ${errBody.error?.message ?? resp.statusText}`,
      );
    }

    return resp.json() as Promise<T>;
  }

  // ── Kontakt-spezifische Abfragen ──────────────────────────────────────

  async getContactEmails(
    userJwt: string,
    userId: string,
    contactEmail: string,
  ): Promise<M365Email[]> {
    const cacheKey = `graph:emails:${userId}:${contactEmail}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365Email[];

    const accessToken = await this.getM365Token(userJwt);

    // Hinweis: $search und $orderby können NICHT kombiniert werden (Graph API Limitation)
    const data = await this.graphGet<{ value: M365Email[] }>(
      accessToken,
      '/me/messages',
      {
        $search: `"${contactEmail}"`,
        $top: '25',
        $select:
          'id,subject,bodyPreview,receivedDateTime,from,hasAttachments,isRead,webLink',
      },
    );

    const emails = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(emails), CACHE_TTL);
    this.logger.debug(`Graph: ${emails.length} E-Mails fuer ${contactEmail} geladen`);
    return emails;
  }

  async getContactCalendar(
    userJwt: string,
    userId: string,
    contactEmail: string,
  ): Promise<M365CalendarEvent[]> {
    const cacheKey = `graph:calendar:${userId}:${contactEmail}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365CalendarEvent[];

    const accessToken = await this.getM365Token(userJwt);

    const now = new Date().toISOString();
    const future = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();

    const data = await this.graphGet<{ value: M365CalendarEvent[] }>(
      accessToken,
      '/me/calendarView',
      {
        startDateTime: now,
        endDateTime: future,
        $top: '20',
        $filter: `attendees/any(a:a/emailAddress/address eq '${contactEmail}')`,
        $select:
          'id,subject,start,end,location,organizer,attendees,isOnlineMeeting,onlineMeetingUrl,webLink',
        $orderby: 'start/dateTime asc',
      },
    );

    const events = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(events), CACHE_TTL);
    return events;
  }

  // ── Globale Office365-Übersicht ───────────────────────────────────────

  /** Alle aktuellen E-Mails (Posteingang) */
  async getAllEmails(userJwt: string, userId: string): Promise<M365Email[]> {
    const cacheKey = `graph:all-emails:${userId}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365Email[];

    const accessToken = await this.getM365Token(userJwt);

    const data = await this.graphGet<{ value: M365Email[] }>(
      accessToken,
      '/me/messages',
      {
        $top: '50',
        $orderby: 'receivedDateTime desc',
        $select:
          'id,subject,bodyPreview,receivedDateTime,from,hasAttachments,isRead,webLink',
      },
    );

    const emails = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(emails), CACHE_TTL);
    this.logger.debug(`Graph: ${emails.length} E-Mails (global) geladen`);
    return emails;
  }

  /** Alle Kalender-Ereignisse (nächste 30 Tage) */
  async getAllCalendarEvents(
    userJwt: string,
    userId: string,
  ): Promise<M365CalendarEvent[]> {
    const cacheKey = `graph:all-calendar:${userId}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365CalendarEvent[];

    const accessToken = await this.getM365Token(userJwt);

    const now = new Date().toISOString();
    const future = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();

    const data = await this.graphGet<{ value: M365CalendarEvent[] }>(
      accessToken,
      '/me/calendarView',
      {
        startDateTime: now,
        endDateTime: future,
        $top: '50',
        $select:
          'id,subject,start,end,location,organizer,attendees,isOnlineMeeting,onlineMeetingUrl,webLink',
        $orderby: 'start/dateTime asc',
      },
    );

    const events = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(events), CACHE_TTL);
    this.logger.debug(`Graph: ${events.length} Kalender-Ereignisse (global) geladen`);
    return events;
  }

  /** Outlook-Kontakte des Benutzers */
  async getAllOutlookContacts(
    userJwt: string,
    userId: string,
  ): Promise<M365Contact[]> {
    const cacheKey = `graph:all-contacts:${userId}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365Contact[];

    const accessToken = await this.getM365Token(userJwt);

    const data = await this.graphGet<{ value: M365Contact[] }>(
      accessToken,
      '/me/contacts',
      {
        $top: '100',
        $orderby: 'displayName asc',
        $select:
          'id,displayName,emailAddresses,mobilePhone,businessPhones,jobTitle,companyName',
      },
    );

    const contacts = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(contacts), CACHE_TTL);
    this.logger.debug(`Graph: ${contacts.length} Outlook-Kontakte geladen`);
    return contacts;
  }

  /** Alle Task-Listen mit Aufgaben */
  async getTasks(userJwt: string, userId: string): Promise<M365TaskList[]> {
    const cacheKey = `graph:tasks:${userId}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365TaskList[];

    const accessToken = await this.getM365Token(userJwt);

    const listsData = await this.graphGet<{
      value: Array<{ id: string; displayName: string }>;
    }>(accessToken, '/me/todo/lists', { $top: '20' });

    const lists: M365TaskList[] = [];

    for (const list of listsData.value ?? []) {
      const tasksData = await this.graphGet<{ value: M365Task[] }>(
        accessToken,
        `/me/todo/lists/${list.id}/tasks`,
        {
          $top: '50',
          $filter: "status ne 'completed'",
          $orderby: 'importance desc',
        },
      );

      lists.push({
        id: list.id,
        displayName: list.displayName,
        tasks: tasksData.value ?? [],
      });
    }

    await this.redis.set(cacheKey, JSON.stringify(lists), CACHE_TTL);
    return lists;
  }

  // ── Kalender: beliebiger Datumsbereich ───────────────────────────────

  /** Kalender-Ereignisse für einen bestimmten Zeitraum (für Monats-/Wochenansicht) */
  async getCalendarEventsForRange(
    userJwt: string,
    userId: string,
    startDate: string, // YYYY-MM-DD (inklusiv)
    endDate: string,   // YYYY-MM-DD (exklusiv)
  ): Promise<M365CalendarEvent[]> {
    const cacheKey = `graph:calendar-range:${userId}:${startDate}:${endDate}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365CalendarEvent[];

    const accessToken = await this.getM365Token(userJwt);

    const data = await this.graphGet<{ value: M365CalendarEvent[] }>(
      accessToken,
      '/me/calendarView',
      {
        startDateTime: `${startDate}T00:00:00Z`,
        endDateTime: `${endDate}T00:00:00Z`,
        $top: '200',
        $select: 'id,subject,start,end,location,organizer,attendees,isOnlineMeeting,onlineMeetingUrl,webLink',
        $orderby: 'start/dateTime asc',
      },
    );

    const events = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(events), CACHE_TTL);
    this.logger.debug(
      `Graph: ${events.length} Kalender-Ereignisse (${startDate} – ${endDate}) geladen`,
    );
    return events;
  }

  // ── Mail-Ordner ───────────────────────────────────────────────────────

  /** Alle Mail-Ordner des Benutzers (Inbox, Gesendet, Entwürfe, …) */
  async getMailFolders(userJwt: string, userId: string): Promise<M365MailFolder[]> {
    const cacheKey = `graph:mail-folders:${userId}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365MailFolder[];

    const accessToken = await this.getM365Token(userJwt);

    const data = await this.graphGet<{ value: M365MailFolder[] }>(
      accessToken,
      '/me/mailFolders',
      {
        $top: '50',
        // wellKnownName wird NICHT in $select aufgenommen — wird von vielen Exchange-Tenants
        // nicht als selektierbares OData-Property unterstützt (400 Bad Request).
        // Ordner-Identifikation erfolgt stattdessen über den displayName.
        $select: 'id,displayName,totalItemCount,unreadItemCount,childFolderCount',
      },
    );

    const folders = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(folders), CACHE_TTL);
    this.logger.debug(`Graph: ${folders.length} Mail-Ordner geladen`);
    return folders;
  }

  /** Alle offenen Aufgaben flach (inkl. Body für CRM-Sync-Erkennung) */
  async getAllTasksFlat(userJwt: string, userId: string): Promise<M365TaskFlat[]> {
    // Kein Cache — Aufgaben müssen immer aktuell sein
    const accessToken = await this.getM365Token(userJwt);

    const listsData = await this.graphGet<{
      value: Array<{ id: string; displayName: string }>;
    }>(accessToken, '/me/todo/lists', { $top: '20' });

    const flatTasks: M365TaskFlat[] = [];

    for (const list of listsData.value ?? []) {
      const tasksData = await this.graphGet<{
        value: Array<{
          id: string;
          title: string;
          status: string;
          importance: string;
          dueDateTime: { dateTime: string; timeZone: string } | null;
          body: { content: string; contentType: string } | null;
          createdDateTime: string;
        }>;
      }>(accessToken, `/me/todo/lists/${list.id}/tasks`, {
        $top: '100',
        $filter: "status ne 'completed'",
        $select: 'id,title,status,importance,dueDateTime,body,createdDateTime',
        $orderby: 'importance desc',
      });

      for (const task of tasksData.value ?? []) {
        flatTasks.push({
          id: task.id,
          listId: list.id,
          listName: list.displayName,
          title: task.title,
          status: task.status,
          importance: task.importance,
          dueDateTime: task.dueDateTime ?? null,
          bodyContent: task.body?.content ?? null,
          createdDateTime: task.createdDateTime,
        });
      }
    }

    this.logger.debug(`Graph: ${flatTasks.length} Aufgaben (flat) fuer ${userId} geladen`);
    return flatTasks;
  }

  /** Neue Aufgabe in Standard-Aufgabenliste erstellen */
  async createM365Task(
    userJwt: string,
    title: string,
    bodyContent?: string,
    dueDateISO?: string,
  ): Promise<{ id: string; listId: string }> {
    const accessToken = await this.getM365Token(userJwt);

    // Standard-Liste finden ("Tasks" / "Aufgaben" / erste verfügbare Liste)
    const listsData = await this.graphGet<{
      value: Array<{ id: string; displayName: string; wellknownListName?: string }>;
    }>(accessToken, '/me/todo/lists', { $top: '20' });

    const lists = listsData.value ?? [];
    const targetList =
      lists.find(
        (l) =>
          l.displayName.toLowerCase() === 'tasks' ||
          l.displayName.toLowerCase() === 'aufgaben' ||
          (l as { wellknownListName?: string }).wellknownListName === 'defaultList',
      ) ?? lists[0];

    if (!targetList) {
      throw new ServiceUnavailableException('Keine M365-Aufgabenliste gefunden');
    }

    const taskPayload: Record<string, unknown> = { title };

    if (bodyContent) {
      taskPayload['body'] = { content: bodyContent, contentType: 'text' };
    }
    if (dueDateISO) {
      taskPayload['dueDateTime'] = { dateTime: dueDateISO, timeZone: 'UTC' };
    }

    const created = await this.graphPost<{ id: string }>(
      accessToken,
      `/me/todo/lists/${targetList.id}/tasks`,
      taskPayload,
    );

    this.logger.debug(`Graph: Aufgabe "${title}" in Liste "${targetList.displayName}" erstellt`);
    return { id: created.id, listId: targetList.id };
  }

  /** Aufgabe als erledigt markieren */
  async completeM365Task(
    userJwt: string,
    listId: string,
    taskId: string,
  ): Promise<void> {
    const accessToken = await this.getM365Token(userJwt);
    await this.graphPatch<unknown>(
      accessToken,
      `/me/todo/lists/${listId}/tasks/${taskId}`,
      { status: 'completed' },
    );
    this.logger.debug(`Graph: Aufgabe ${taskId} als erledigt markiert`);
  }

  /** Microsoft-365-Benutzerprofil laden (für Profilanreicherung) */
  async getM365Profile(userJwt: string): Promise<M365UserProfile> {
    const accessToken = await this.getM365Token(userJwt);

    const data = await this.graphGet<{
      givenName?: string | null;
      surname?: string | null;
      displayName?: string | null;
      mobilePhone?: string | null;
      businessPhones?: string[];
      city?: string | null;
      streetAddress?: string | null;
      postalCode?: string | null;
      jobTitle?: string | null;
      department?: string | null;
      companyName?: string | null;
      officeLocation?: string | null;
    }>(accessToken, '/me', {
      $select:
        'givenName,surname,displayName,mobilePhone,businessPhones,city,streetAddress,postalCode,jobTitle,department,companyName,officeLocation',
    });

    return {
      givenName: data.givenName ?? null,
      surname: data.surname ?? null,
      displayName: data.displayName ?? null,
      mobilePhone: data.mobilePhone ?? null,
      businessPhones: data.businessPhones ?? [],
      city: data.city ?? null,
      streetAddress: data.streetAddress ?? null,
      postalCode: data.postalCode ?? null,
      jobTitle: data.jobTitle ?? null,
      department: data.department ?? null,
      companyName: data.companyName ?? null,
      officeLocation: data.officeLocation ?? null,
    };
  }

  /**
   * CRM-Kontakt in Outlook-Kontakte pushen / synchronisieren.
   * Sucht anhand der E-Mail-Adresse nach einem vorhandenen Outlook-Kontakt.
   * Wenn vorhanden → PATCH (Update), sonst → POST (Neu anlegen).
   */
  async pushContactToOutlook(
    userJwt: string,
    contact: {
      firstName: string | null;
      lastName: string | null;
      companyName: string | null;
      email: string | null;
      phone: string | null;
      mobile: string | null;
      position: string | null;
      department: string | null;
      street: string | null;
      zip: string | null;
      city: string | null;
      state: string | null;
      country: string | null;
      website: string | null;
      notes: string | null;
    },
  ): Promise<{ created: boolean; outlookContactId: string }> {
    const accessToken = await this.getM365Token(userJwt);

    const displayName = [contact.firstName, contact.lastName]
      .filter(Boolean)
      .join(' ') || contact.companyName || '';

    const outlookPayload: Record<string, unknown> = {
      givenName: contact.firstName ?? '',
      surname: contact.lastName ?? '',
      jobTitle: contact.position ?? '',
      department: contact.department ?? '',
      companyName: contact.companyName ?? '',
      businessHomePage: contact.website ?? '',
      personalNotes: contact.notes ?? '',
    };

    if (contact.email) {
      outlookPayload['emailAddresses'] = [
        { address: contact.email, name: displayName },
      ];
    }

    const businessPhones: string[] = [];
    if (contact.phone) businessPhones.push(contact.phone);
    if (businessPhones.length > 0) {
      outlookPayload['businessPhones'] = businessPhones;
    }
    if (contact.mobile) {
      outlookPayload['mobilePhone'] = contact.mobile;
    }

    if (contact.street || contact.zip || contact.city) {
      outlookPayload['businessAddress'] = {
        street: contact.street ?? '',
        city: contact.city ?? '',
        state: contact.state ?? '',
        postalCode: contact.zip ?? '',
        countryOrRegion: contact.country ?? '',
      };
    }

    // Vorhandenen Outlook-Kontakt anhand der E-Mail suchen
    let existingId: string | null = null;
    if (contact.email) {
      try {
        const search = await this.graphGet<{ value: Array<{ id: string }> }>(
          accessToken,
          '/me/contacts',
          {
            $filter: `emailAddresses/any(a:a/address eq '${contact.email}')`,
            $top: '1',
            $select: 'id',
          },
        );
        existingId = search.value?.[0]?.id ?? null;
      } catch {
        // Suche schlägt fehl → Neuanlage
      }
    }

    if (existingId) {
      await this.graphPatch<unknown>(
        accessToken,
        `/me/contacts/${existingId}`,
        outlookPayload,
      );
      this.logger.debug(
        `Graph: CRM-Kontakt in Outlook aktualisiert (${existingId})`,
      );
      return { created: false, outlookContactId: existingId };
    }

    const created = await this.graphPost<{ id: string }>(
      accessToken,
      '/me/contacts',
      outlookPayload,
    );
    this.logger.debug(
      `Graph: CRM-Kontakt in Outlook erstellt (${created.id})`,
    );
    return { created: true, outlookContactId: created.id };
  }

  /**
   * Microsoft-365-Profilbild laden (96x96 JPEG).
   * Gibt Base64 Data-URL zurück, oder null wenn kein Foto vorhanden (404).
   */
  async getM365Photo(userJwt: string): Promise<string | null> {
    const accessToken = await this.getM365Token(userJwt);

    try {
      const resp = await fetch(
        `${GRAPH_BASE}/me/photos/96x96/$value`,
        {
          headers: { Authorization: `Bearer ${accessToken}` },
          signal: AbortSignal.timeout(10000),
        },
      );

      if (resp.status === 404 || resp.status === 400) {
        this.logger.debug('Graph: Kein M365-Profilbild vorhanden (404/400)');
        return null;
      }

      if (!resp.ok) {
        this.logger.warn(`Graph: Profilbild-Fehler ${resp.status} — wird ignoriert`);
        return null;
      }

      const arrayBuffer = await resp.arrayBuffer();
      const base64 = Buffer.from(arrayBuffer).toString('base64');
      return `data:image/jpeg;base64,${base64}`;
    } catch (err) {
      // Foto ist optional — Fehler niemals an den User propagieren
      this.logger.warn(`Graph: getM365Photo Fehler: ${(err as Error).message}`);
      return null;
    }
  }

  /** E-Mails in einem bestimmten Ordner (mit optionalem Tages-Filter) */
  async getMailsByFolder(
    userJwt: string,
    userId: string,
    folderId: string,
    days: number,
  ): Promise<M365Email[]> {
    const cacheKey = `graph:folder-mails:${userId}:${folderId}:${days}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as M365Email[];

    const accessToken = await this.getM365Token(userJwt);

    const params: Record<string, string> = {
      $top: '50',
      $orderby: 'receivedDateTime desc',
      $select: 'id,subject,bodyPreview,receivedDateTime,from,hasAttachments,isRead,webLink',
    };

    if (days > 0) {
      const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
      params['$filter'] = `receivedDateTime ge ${since}`;
    }

    const data = await this.graphGet<{ value: M365Email[] }>(
      accessToken,
      `/me/mailFolders/${folderId}/messages`,
      params,
    );

    const emails = data.value ?? [];
    await this.redis.set(cacheKey, JSON.stringify(emails), CACHE_TTL);
    this.logger.debug(
      `Graph: ${emails.length} E-Mails in Ordner ${folderId} (days=${days}) geladen`,
    );
    return emails;
  }
}
