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[];
}

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