/**
 * Microsoft Graph API Service
 * - Client credentials flow for mailbox access (replaces IMAP)
 * - Reads unread emails from the shared IT support mailbox
 */

const Ticket = require('../models/Ticket');
const TicketComment = require('../models/TicketComment');
const EmailService = require('./email.service');
const ExternalAlert = require('../models/ExternalAlert');
const AiService = require('./ai.service');
const { parseNetgoAlert, isRecovery } = require('./netgoParser');
const { parseArcticWolfAlert }        = require('./arcticWolfParser');

const BOUNCE_SUBJECT_PATTERNS = [
    /^undeliverable:/i, /^unzustellbar:/i,
    /^delivery\s+(status\s+notification|failed|failure)/i,
    /^mail\s+delivery\s+(failed|subsystem)/i,
    /^failure\s+notice/i, /^non.?delivery/i,
    /^returned\s+mail/i, /^auto.?reply:/i,
    /^automatic\s+reply:/i, /^out\s+of\s+(office|the\s+office)/i,
    /^abwesenheit/i, /^abwesenheitsnotiz/i,
];
const MAILER_DAEMON_PATTERNS = [
    /^mailer-daemon@/i, /^postmaster@/i,
    /^noreply@/i, /^no-reply@/i,
    /^do-not-reply@/i, /^donotreply@/i,
    /^mail-daemon@/i, /^bounce@/i, /^bounce\+/i,
    /^nospamproxy@/i,
];

function isBounceOrAutoReply(subject, fromAddress) {
    if (BOUNCE_SUBJECT_PATTERNS.some(p => p.test(subject))) return true;
    if (fromAddress && MAILER_DAEMON_PATTERNS.some(p => p.test(fromAddress))) return true;
    return false;
}

async function getClientToken() {
    const res = await fetch(
        `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
        {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                client_id: process.env.AZURE_CLIENT_ID,
                client_secret: process.env.AZURE_CLIENT_SECRET,
                grant_type: 'client_credentials',
                scope: 'https://graph.microsoft.com/.default',
            }),
        }
    );

    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Token request failed: ${err}`);
    }

    const data = await res.json();
    return data.access_token;
}

async function pollMailbox() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        return; // Graph API not configured
    }

    const mailbox = process.env.IMAP_USER;
    if (!mailbox) return;

    try {
        const token = await getClientToken();

        // Get unread messages from inbox
        const res = await fetch(
            `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(mailbox)}/mailFolders/inbox/messages` +
            `?$filter=isRead eq false&$select=id,subject,from,body,receivedDateTime,internetMessageId&$top=20`,
            { headers: { Authorization: `Bearer ${token}` } }
        );

        if (!res.ok) {
            const err = await res.text();
            console.error('[Graph] Failed to fetch messages:', err);
            return;
        }

        const data = await res.json();
        const messages = data.value || [];

        if (messages.length === 0) return;

        console.log(`[Graph] Processing ${messages.length} unread message(s)`);

        for (const msg of messages) {
            try {
                const messageId = msg.internetMessageId;

                // Deduplication check
                if (messageId && Ticket.getByEmailMessageId(messageId)) {
                    await markAsRead(token, mailbox, msg.id);
                    continue;
                }

                const fromEmail = (msg.from?.emailAddress?.address || '').toLowerCase();
                const fromName = msg.from?.emailAddress?.name || fromEmail;
                const subject = msg.subject || '(Kein Betreff)';

                // Bounce/Auto-Reply-Erkennung
                if (isBounceOrAutoReply(subject, fromEmail)) {
                    console.log(`[Graph] Bounce/Auto-Reply übersprungen: "${subject}" von ${fromEmail}`);
                    await markAsRead(token, mailbox, msg.id);
                    continue;
                }

                // Extract plain text from body
                let bodyText = '';
                if (msg.body?.contentType === 'text') {
                    bodyText = msg.body.content || '';
                } else if (msg.body?.content) {
                    bodyText = msg.body.content
                        .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
                        .replace(/<[^>]+>/g, '')
                        .replace(/&nbsp;/g, ' ')
                        .replace(/&amp;/g, '&')
                        .replace(/&lt;/g, '<')
                        .replace(/&gt;/g, '>')
                        .replace(/\r\n|\r/g, '\n')
                        .replace(/\n{3,}/g, '\n\n')
                        .trim();
                }

                // Check for Netgo monitoring email
                const netgoAlert = parseNetgoAlert(fromEmail, subject, bodyText);
                if (netgoAlert) {
                    if (!ExternalAlert.getByEmailMessageId(messageId)) {
                        ExternalAlert.create({ ...netgoAlert, raw_body: bodyText.substring(0, 5000), email_message_id: messageId });
                        console.log(`[Graph] Netgo Alert gespeichert: ${netgoAlert.device} – ${netgoAlert.severity}`);
                    }
                    await markAsRead(token, mailbox, msg.id);
                    continue;
                }

                // Check for Arctic Wolf security email
                const awAlert = parseArcticWolfAlert(fromEmail, subject, bodyText);
                if (awAlert) {
                    if (!ExternalAlert.getByEmailMessageId(messageId)) {
                        ExternalAlert.create({ ...awAlert, raw_body: bodyText.substring(0, 5000), email_message_id: messageId });
                        console.log(`[Graph] Arctic Wolf Alert gespeichert: ${awAlert.device} – ${awAlert.severity}`);
                    }
                    await markAsRead(token, mailbox, msg.id);
                    continue;
                }

                // Reply auf bestehendes Ticket? → Ticketnummer aus Betreff [TK-YYYY-NNNN]
                const ticketRefMatch = subject.match(/\[?(TK-\d{4}-\d{4})\]?/i);
                if (ticketRefMatch) {
                    const existing = Ticket.getByTicketNumber(ticketRefMatch[1].toUpperCase());
                    if (existing) {
                        TicketComment.create(existing.id, null, bodyText.substring(0, 2000) || '(E-Mail Antwort)', false);
                        if (existing.status === 'geschlossen') {
                            Ticket.update(existing.id, { status: 'offen' });
                            console.log(`[Graph] Ticket ${ticketRefMatch[1]} durch E-Mail-Antwort wieder geöffnet`);
                        }
                        await markAsRead(token, mailbox, msg.id);
                        console.log(`[Graph] Antwort auf Ticket ${ticketRefMatch[1]} als Kommentar hinzugefügt von ${fromEmail}`);
                        continue;
                    }
                }

                // Create ticket
                const ticket = Ticket.create({
                    title: subject.replace(/^(re|aw|fwd?|wg):\s*/i, '').substring(0, 200),
                    description: bodyText.substring(0, 5000),
                    source: 'email',
                    requester_email: fromEmail,
                    requester_name: fromName,
                    email_message_id: messageId,
                    category: 'Allgemein',
                    priority: 'mittel',
                    status: 'offen',
                });

                console.log(`[Graph] Ticket created: ${ticket.ticket_number} from ${fromEmail}`);

                // AI-Analyse: Kategorie, Priorität und Lösungsvorschlag
                try {
                    if (AiService.isConfigured()) {
                        const analysis = await AiService.analyzeTicket(ticket.title, ticket.description);
                        Ticket.update(ticket.id, {
                            category: analysis.category,
                            priority: analysis.priority,
                            ai_suggestion: analysis.suggestion,
                        });
                        console.log(`[Graph] AI-Analyse für ${ticket.ticket_number}: ${analysis.category}/${analysis.priority}`);
                    }
                } catch (aiErr) {
                    console.error('[Graph] AI-Analyse fehlgeschlagen:', aiErr.message);
                }

                // Send confirmation to requester
                try {
                    await EmailService.sendTicketCreatedConfirmation(ticket);
                } catch (emailErr) {
                    console.error('[Graph] Failed to send confirmation:', emailErr.message);
                }

                // Mark as read
                await markAsRead(token, mailbox, msg.id);

            } catch (msgErr) {
                console.error('[Graph] Error processing message:', msgErr.message);
            }
        }
    } catch (err) {
        console.error('[Graph] Poll error:', err.message);
    }
}

async function pollMonitoringMailbox() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) return;
    const mailbox = process.env.MONITORING_MAILBOX;
    if (!mailbox) return;

    try {
        const token = await getClientToken();

        const res = await fetch(
            `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(mailbox)}/mailFolders/inbox/messages` +
            `?$filter=isRead eq false&$select=id,subject,from,body,receivedDateTime,internetMessageId&$top=20`,
            { headers: { Authorization: `Bearer ${token}` } }
        );

        if (!res.ok) {
            const err = await res.text();
            console.error('[Graph/Monitoring] Failed to fetch messages:', err);
            return;
        }

        const messages = (await res.json()).value || [];
        if (messages.length === 0) return;

        for (const msg of messages) {
            try {
                const fromEmail = msg.from?.emailAddress?.address || '';
                const subject   = msg.subject || '';
                const messageId = msg.internetMessageId;

                let bodyText = '';
                if (msg.body?.contentType === 'text') {
                    bodyText = msg.body.content || '';
                } else if (msg.body?.content) {
                    bodyText = msg.body.content
                        .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
                        .replace(/<br\s*\/?>/gi, '\n')
                        .replace(/<\/?(p|div|tr|td|th|li|h[1-6])[^>]*>/gi, '\n')
                        .replace(/<[^>]+>/g, '')
                        .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&')
                        .replace(/&lt;/g, '<').replace(/&gt;/g, '>')
                        .replace(/\r\n|\r/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
                }

                const netgoAlert = parseNetgoAlert(fromEmail, subject, bodyText);
                if (netgoAlert) {
                    if (!messageId || !ExternalAlert.getByEmailMessageId(messageId)) {
                        const created = ExternalAlert.create({ ...netgoAlert, raw_body: bodyText.substring(0, 5000), email_message_id: messageId });
                        console.log(`[Graph/Monitoring] Netgo Alert: ${netgoAlert.device} – ${netgoAlert.severity}`);
                        // KI-Analyse asynchron (nicht blockierend)
                        if (created && AiService.isConfigured()) {
                            AiService.analyzeNetgoAlert(netgoAlert).then(analysis => {
                                if (analysis) ExternalAlert.setAiAnalysis(created.id, analysis);
                            }).catch(() => {});
                        }
                    }
                    await markAsRead(token, mailbox, msg.id);
                    continue;
                }

                const awAlert = parseArcticWolfAlert(fromEmail, subject, bodyText);
                if (awAlert) {
                    if (!messageId || !ExternalAlert.getByEmailMessageId(messageId)) {
                        ExternalAlert.create({ ...awAlert, raw_body: bodyText.substring(0, 5000), email_message_id: messageId });
                        console.log(`[Graph/Monitoring] Arctic Wolf Alert: ${awAlert.device} – ${awAlert.severity}`);
                    }
                    await markAsRead(token, mailbox, msg.id);
                }
                // Non-monitoring emails: ignore completely (leave unread, no ticket)
            } catch (msgErr) {
                console.error('[Graph/Monitoring] Error processing message:', msgErr.message);
            }
        }
    } catch (err) {
        console.error('[Graph/Monitoring] Poll error:', err.message);
    }
}

async function markAsRead(token, mailbox, messageId) {
    try {
        await fetch(
            `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(mailbox)}/messages/${messageId}`,
            {
                method: 'PATCH',
                headers: {
                    Authorization: `Bearer ${token}`,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ isRead: true }),
            }
        );
    } catch (err) {
        console.error('[Graph] Failed to mark as read:', err.message);
    }
}

/**
 * Fetch all Azure AD groups.
 * Requires application permission: Group.Read.All
 */
async function getAzureGroups() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)');
    }

    const token = await getClientToken();
    const groups = [];
    let url = 'https://graph.microsoft.com/v1.0/groups?$select=id,displayName,description&$top=100&$orderby=displayName';

    while (url) {
        const res = await fetch(url, {
            headers: { Authorization: `Bearer ${token}` }
        });

        if (!res.ok) {
            const err = await res.text();
            throw new Error(`Gruppen-Abfrage fehlgeschlagen: ${err}`);
        }

        const data = await res.json();
        groups.push(...(data.value || []));
        url = data['@odata.nextLink'] || null;
    }

    return groups;
}

/**
 * Fetch all members of a specific Azure AD group.
 * Requires application permission: Group.Read.All, User.Read.All
 */
async function getAzureGroupMembers(groupId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)');
    }

    const token = await getClientToken();
    const members = [];
    let url = `https://graph.microsoft.com/v1.0/groups/${encodeURIComponent(groupId)}/members` +
        `?$select=id,displayName,givenName,surname,mail,userPrincipalName&$top=100`;

    while (url) {
        const res = await fetch(url, {
            headers: { Authorization: `Bearer ${token}` }
        });

        if (!res.ok) {
            const err = await res.text();
            throw new Error(`Gruppen-Mitglieder-Abfrage fehlgeschlagen: ${err}`);
        }

        const data = await res.json();
        // Filter to only user objects (groups can also contain devices/groups)
        const users = (data.value || []).filter(m => m['@odata.type'] === '#microsoft.graph.user' || m.userPrincipalName);
        members.push(...users);
        url = data['@odata.nextLink'] || null;
    }

    return members;
}

/**
 * Fetch all managed devices from Microsoft Intune via Graph API.
 * Requires application permission: DeviceManagementManagedDevices.Read.All
 * Returns an array of raw managedDevice objects.
 */
async function getIntuneManagedDevices() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)');
    }

    const token = await getClientToken();
    const devices = [];
    let url = 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices' +
        '?$select=id,deviceName,serialNumber,model,manufacturer,operatingSystem,managedDeviceOwnerType,managementState' +
        '&$top=100';

    while (url) {
        const res = await fetch(url, {
            headers: { Authorization: `Bearer ${token}` }
        });

        if (!res.ok) {
            const err = await res.text();
            throw new Error(`Intune-Abfrage fehlgeschlagen: ${err}`);
        }

        const data = await res.json();
        devices.push(...(data.value || []));
        url = data['@odata.nextLink'] || null;
    }

    return devices;
}

/**
 * Fetch subscribed Microsoft 365 / Entra licenses for the tenant.
 * Requires application permission: Organization.Read.All (or LicenseAssignment.ReadWrite.All)
 */
async function getEntraSubscribedSkus() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)');
    }

    const token = await getClientToken();
    const res = await fetch(
        'https://graph.microsoft.com/v1.0/subscribedSkus?$select=skuId,skuPartNumber,capabilityStatus,consumedUnits,prepaidUnits,servicePlans',
        { headers: { Authorization: `Bearer ${token}` } }
    );

    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Entra-Lizenz-Abfrage fehlgeschlagen: ${err}`);
    }

    const data = await res.json();
    return data.value || [];
}

/**
 * Fetch all users assigned to a specific Microsoft 365 / Entra license SKU.
 * Requires application permission: User.Read.All
 */
async function getEntraLicenseUsers(skuId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)');
    }

    const token = await getClientToken();
    const users = [];
    let url = `https://graph.microsoft.com/v1.0/users` +
        `?$filter=assignedLicenses/any(x:x/skuId eq ${skuId})` +
        `&$select=id,displayName,mail,userPrincipalName,accountEnabled` +
        `&$top=100&$count=true`;

    while (url) {
        const res = await fetch(url, {
            headers: {
                Authorization: `Bearer ${token}`,
                ConsistencyLevel: 'eventual',
            }
        });

        if (!res.ok) {
            const err = await res.text();
            throw new Error(`Entra-Benutzer-Abfrage fehlgeschlagen: ${err}`);
        }

        const data = await res.json();
        users.push(...(data.value || []));
        url = data['@odata.nextLink'] || null;
    }

    return users;
}

/**
 * Find the manager for a given department by querying Entra users.
 * Strategy: get users whose department attribute matches, then fetch their manager.
 * Requires: User.Read.All
 */
async function getDepartmentManager(department) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    if (!department) throw new Error('Kein Abteilungsname angegeben');

    const token = await getClientToken();

    // Find users in this department
    const usersRes = await fetch(
        `https://graph.microsoft.com/v1.0/users` +
        `?$filter=department eq '${department.replace(/'/g, "''")}'` +
        `&$select=id,displayName,mail,userPrincipalName,department&$top=5`,
        { headers: { Authorization: `Bearer ${token}`, ConsistencyLevel: 'eventual' } }
    );

    if (!usersRes.ok) {
        const err = await usersRes.text();
        throw new Error(`Benutzer-Abfrage fehlgeschlagen: ${err}`);
    }

    const usersData = await usersRes.json();
    const deptUsers = (usersData.value || []).filter(u => u.userPrincipalName);

    if (deptUsers.length === 0) {
        return null; // No users found for this department
    }

    // Get manager of the first user (assume same manager for whole dept)
    const managersFound = new Map();
    for (const u of deptUsers.slice(0, 3)) {
        try {
            const mgrRes = await fetch(
                `https://graph.microsoft.com/v1.0/users/${u.id}/manager?$select=id,displayName,mail,userPrincipalName,jobTitle`,
                { headers: { Authorization: `Bearer ${token}` } }
            );
            if (mgrRes.ok) {
                const mgr = await mgrRes.json();
                if (mgr.id) {
                    managersFound.set(mgr.id, mgr);
                }
            }
        } catch { /* ignore individual failures */ }
    }

    if (managersFound.size === 0) return null;

    // Return the most common manager (or first found)
    const [manager] = managersFound.values();
    return {
        entraId: manager.id,
        displayName: manager.displayName,
        mail: manager.mail || manager.userPrincipalName,
        jobTitle: manager.jobTitle,
    };
}

/**
 * Search Entra users by display name (partial match).
 * Uses $search which requires ConsistencyLevel: eventual.
 */
async function searchEntraUserByName(name) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    if (!name || name.trim().length < 2) throw new Error('Mindestens 2 Zeichen eingeben');

    const token = await getClientToken();
    const encoded = encodeURIComponent(`"${name.trim().replace(/"/g, '')}"`);

    const res = await fetch(
        `https://graph.microsoft.com/v1.0/users?$search=displayName:${encoded}` +
        `&$select=id,displayName,mail,userPrincipalName,jobTitle,department&$top=10&$orderby=displayName`,
        { headers: { Authorization: `Bearer ${token}`, ConsistencyLevel: 'eventual' } }
    );

    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Entra-Suche fehlgeschlagen: ${err}`);
    }

    const data = await res.json();
    return (data.value || []).map(u => ({
        entraId: u.id,
        displayName: u.displayName,
        mail: u.mail || u.userPrincipalName,
        jobTitle: u.jobTitle || null,
        department: u.department || null,
    }));
}

// ─── Microsoft Planner ────────────────────────────────────────────────────────

const PRIORITY_MAP = { 'Kritisch': 1, 'Hoch': 3, 'Normal': 5, 'Niedrig': 9 };
const STATUS_MAP   = { 'Offen': 0, 'In Planung': 10, 'In Umsetzung': 50, 'Abgeschlossen': 100 };

// Reverse: percentComplete → IT-Nexus Status (only for new imports without existing status)
function percentToStatusForImport(percent) {
    if (percent >= 100) return 'Abgeschlossen';
    if (percent >= 50)  return 'In Umsetzung';
    if (percent >= 1)   return 'In Planung';
    return 'Offen';
}

// Pull logic: IT-Nexus is the source of truth.
// Planner may only mark a task as "Abgeschlossen" (100%) — everything else keeps IT-Nexus status.
function plannerPullStatus(percent, currentStatus) {
    if (percent >= 100) return 'Abgeschlossen';
    return currentStatus; // IT-Nexus wins for all other states
}

async function plannerRequest(method, path, body = null) {
    const token = await getClientToken();
    const opts = {
        method,
        headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    };
    if (body) opts.body = JSON.stringify(body);
    const res = await fetch(`https://graph.microsoft.com/v1.0${path}`, opts);
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Planner API ${method} ${path} fehlgeschlagen: ${err}`);
    }
    if (res.status === 204) return null;
    return res.json();
}

async function getPlannerTaskWithEtag(taskId) {
    const token = await getClientToken();
    const res = await fetch(`https://graph.microsoft.com/v1.0/planner/tasks/${taskId}`, {
        headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Planner-Task nicht gefunden: ${err}`);
    }
    const data = await res.json();
    const etag = res.headers.get('ETag') || data['@odata.etag'] || '';
    return { data, etag };
}

async function getPlannerBuckets(planId) {
    const data = await plannerRequest('GET', `/planner/plans/${planId}/buckets`);
    return data.value || [];
}

async function createPlannerBucket(planId, name) {
    return plannerRequest('POST', '/planner/buckets', { name, planId, orderHint: ' !' });
}

async function getPlannerTasks(planId) {
    const data = await plannerRequest('GET', `/planner/plans/${planId}/tasks`);
    return data.value || [];
}

async function createPlannerTask(planId, bucketId, topic) {
    const body = {
        planId,
        bucketId,
        title: topic.title,
        percentComplete: STATUS_MAP[topic.status] ?? 0,
        priority: PRIORITY_MAP[topic.priority] ?? 5,
    };
    if (topic.target_date) body.dueDateTime = new Date(topic.target_date).toISOString();
    return plannerRequest('POST', '/planner/tasks', body);
}

async function updatePlannerTask(taskId, etag, topic) {
    const token = await getClientToken();
    const body = {
        title: topic.title,
        percentComplete: STATUS_MAP[topic.status] ?? 0,
        priority: PRIORITY_MAP[topic.priority] ?? 5,
        dueDateTime: topic.target_date ? new Date(topic.target_date).toISOString() : null,
    };
    const res = await fetch(`https://graph.microsoft.com/v1.0/planner/tasks/${taskId}`, {
        method: 'PATCH',
        headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
            'If-Match': etag,
        },
        body: JSON.stringify(body),
    });
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Planner-Task-Update fehlgeschlagen: ${err}`);
    }
}

/**
 * Bidirectional sync between IT-Nexus and a Planner plan.
 *
 * Phase 1 – PULL: For each topic with a known planner_task_id, fetch the
 *   current Planner task. If the GF has changed percentComplete, update the
 *   IT-Nexus status accordingly (Planner wins for status).
 *
 * Phase 2 – PUSH: Push all topics (with up-to-date status from Phase 1)
 *   to Planner: create missing tasks, update existing ones.
 *
 * Returns { imported, pulled, created, updated, errors }
 */
async function syncItTopicsToPlanner(topics, planId) {
    const ItTopic = require('../models/ItTopic');
    const CATEGORIES = ['Infrastruktur & Betrieb', 'Benutzer & Arbeitsplätze', 'Systeme', 'IT-Governance & Compliance', 'Archiv'];
    const result = { imported: 0, pulled: 0, created: 0, updated: 0, errors: [] };

    // ── Phase 1: PULL – import new Planner tasks + sync status changes ──────
    const existingBuckets = await getPlannerBuckets(planId);

    // Build bucket maps: id→category and category→id
    const bucketIdToCategory = {};
    const bucketMap = {};
    for (const bucket of existingBuckets) {
        if (CATEGORIES.includes(bucket.name)) {
            bucketIdToCategory[bucket.id] = bucket.name;
            bucketMap[bucket.name] = bucket.id;
        }
    }
    // Create missing buckets
    for (const cat of CATEGORIES) {
        if (!bucketMap[cat]) {
            try {
                const created = await createPlannerBucket(planId, cat);
                bucketMap[cat] = created.id;
                bucketIdToCategory[created.id] = cat;
            } catch (e) {
                result.errors.push(`Bucket "${cat}": ${e.message}`);
            }
        }
    }

    // All Planner tasks in this plan
    const allPlannerTasks = await getPlannerTasks(planId);

    // Known task IDs already linked in IT-Nexus
    const knownTaskIds = new Set(topics.map(t => t.planner_task_id).filter(Boolean));

    // Import new tasks from Planner that don't exist in IT-Nexus
    for (const plannerTask of allPlannerTasks) {
        if (knownTaskIds.has(plannerTask.id)) continue;
        const category = bucketIdToCategory[plannerTask.bucketId];
        if (!category) continue; // task in unknown bucket — skip
        try {
            const newTopic = ItTopic.create({
                title: plannerTask.title,
                category,
                status: percentToStatusForImport(plannerTask.percentComplete ?? 0),
                priority: 'Normal',
                responsible: '',
                target_date: plannerTask.dueDateTime ? plannerTask.dueDateTime.split('T')[0] : '',
                description: '',
                notes: '',
            });
            ItTopic.update(newTopic.id, { planner_task_id: plannerTask.id });
            // Add to topics so Phase 2 won't push it back as a new task
            topics = [...topics, { ...newTopic, planner_task_id: plannerTask.id }];
            knownTaskIds.add(plannerTask.id);
            result.imported++;
            console.log(`[Planner] Import: "${plannerTask.title}" (${category})`);
        } catch (e) {
            result.errors.push(`Import "${plannerTask.title}": ${e.message}`);
        }
    }

    // Build mutable topic map for status-pull phase
    const topicMap = {};
    for (const topic of topics) {
        topicMap[topic.id] = { ...topic };
    }

    // Sync status changes for known tasks
    for (const topic of topics) {
        if (!topic.planner_task_id) continue;
        try {
            const { data: plannerTask } = await getPlannerTaskWithEtag(topic.planner_task_id);
            // IT-Nexus wins — Planner may only set "Abgeschlossen" (100%)
            const plannerStatus = plannerPullStatus(plannerTask.percentComplete ?? 0, topic.status);
            if (plannerStatus !== topic.status) {
                ItTopic.update(topic.id, { status: plannerStatus });
                topicMap[topic.id].status = plannerStatus;
                result.pulled++;
                console.log(`[Planner] Pull: "${topic.title}" ${topic.status} → ${plannerStatus}`);
            }
        } catch (e) {
            // Task may have been deleted — handled in Push phase
        }
    }

    const updatedTopics = Object.values(topicMap);

    // ── Phase 2: PUSH to Planner ────────────────────────────────────────────
    for (const topic of updatedTopics) {
        const bucketId = bucketMap[topic.category];
        if (!bucketId) {
            result.errors.push(`Kein Bucket für Kategorie "${topic.category}"`);
            continue;
        }
        try {
            if (!topic.planner_task_id) {
                const task = await createPlannerTask(planId, bucketId, topic);
                ItTopic.update(topic.id, { planner_task_id: task.id });
                result.created++;
            } else {
                try {
                    const { etag } = await getPlannerTaskWithEtag(topic.planner_task_id);
                    await updatePlannerTask(topic.planner_task_id, etag, topic);
                    result.updated++;
                } catch (e) {
                    // Task was deleted in Planner — recreate
                    const task = await createPlannerTask(planId, bucketId, topic);
                    ItTopic.update(topic.id, { planner_task_id: task.id });
                    result.created++;
                }
            }
        } catch (e) {
            result.errors.push(`"${topic.title}": ${e.message}`);
        }
    }

    return result;
}

/**
 * Fetch Microsoft Secure Score (latest snapshot).
 * Requires: SecurityEvents.Read.All or SecurityActions.Read.All
 */
async function getSecureScore() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        'https://graph.microsoft.com/v1.0/security/secureScores?$top=1',
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) throw new Error(`Secure Score Fehler: ${await res.text()}`);
    const data = await res.json();
    return data.value?.[0] || null;
}

/**
 * Fetch risky users from Identity Protection.
 * Requires: IdentityRiskEvent.Read.All (Azure AD Premium P2)
 */
async function getRiskyUsers() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?$filter=riskState eq \'atRisk\'&$top=50',
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) throw new Error(`Risky Users Fehler: ${await res.text()}`);
    const data = await res.json();
    return data.value || [];
}

/**
 * Fetch active Defender / security alerts.
 * Requires: SecurityAlert.Read.All
 */
async function getDefenderAlerts() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        'https://graph.microsoft.com/v1.0/security/alerts_v2?$filter=status ne \'resolved\'&$top=50&$orderby=createdDateTime desc',
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) throw new Error(`Fehlende Berechtigung: SecurityAlert.Read.All in Azure App Registration erforderlich`);
    const data = await res.json();
    return data.value || [];
}

/**
 * Fetch non-compliant Intune devices.
 * Requires: DeviceManagementManagedDevices.Read.All
 */
async function getNonCompliantDevices() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$filter=complianceState eq \'noncompliant\'&$select=id,deviceName,serialNumber,operatingSystem,complianceState,userDisplayName,lastSyncDateTime&$top=50',
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) throw new Error(`Non-Compliant Devices Fehler: ${await res.text()}`);
    const data = await res.json();
    return data.value || [];
}

/**
 * Fetch users without MFA registered.
 * Requires: AuditLog.Read.All
 */
async function getUsersWithoutMfa() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        'https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails?$filter=isMfaRegistered eq false and userType eq \'member\'&$top=50',
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) throw new Error(`Fehlende Berechtigung: AuditLog.Read.All in Azure App Registration erforderlich`);
    const data = await res.json();
    return data.value || [];
}

/**
 * Fetch all Entra ID users with basic profile info.
 * Requires: User.Read.All
 */
async function getEntraUsers() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const users = [];
    let url = 'https://graph.microsoft.com/v1.0/users' +
        '?$select=id,displayName,mail,userPrincipalName,jobTitle,department,accountEnabled' +
        '&$top=100&$orderby=displayName&$count=true';

    while (url) {
        const res = await fetch(url, {
            headers: { Authorization: `Bearer ${token}`, ConsistencyLevel: 'eventual' },
        });
        if (!res.ok) throw new Error(`Benutzer-Abfrage fehlgeschlagen: Berechtigung User.Read.All erforderlich`);
        const data = await res.json();
        users.push(...(data.value || []));
        url = data['@odata.nextLink'] || null;
    }
    return users;
}

/**
 * Fetch MFA registration details for ALL users (not just non-MFA).
 * Requires: AuditLog.Read.All
 */
async function getAllMfaRegistrationDetails() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const results = [];
    let url = 'https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails' +
        '?$filter=userType eq \'member\'&$top=100&$select=id,userDisplayName,userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered';

    while (url) {
        const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
        if (!res.ok) throw new Error(`MFA-Details fehlgeschlagen: AuditLog.Read.All erforderlich`);
        const data = await res.json();
        results.push(...(data.value || []));
        url = data['@odata.nextLink'] || null;
    }
    return results;
}

/**
 * Fetch groups the user is member of.
 * Requires: User.Read.All, GroupMember.Read.All
 */
async function getEntraUserGroups(userId) {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/users/${userId}/memberOf` +
        `?$select=id,displayName,description,groupTypes&$top=100`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) throw new Error(`Benutzer-Gruppen fehlgeschlagen: GroupMember.Read.All erforderlich`);
    const data = await res.json();
    return (data.value || []).filter(g => g['@odata.type'] === '#microsoft.graph.group');
}

/**
 * Fetch all activated directory roles with their members.
 * Requires: RoleManagement.Read.Directory
 */
async function getDirectoryRolesWithMembers() {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();

    // Get activated roles
    const rolesRes = await fetch(
        'https://graph.microsoft.com/v1.0/directoryRoles?$select=id,displayName,description,roleTemplateId',
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!rolesRes.ok) throw new Error(`Rollen-Abfrage fehlgeschlagen: RoleManagement.Read.Directory erforderlich`);
    const rolesData = await rolesRes.json();
    const roles = rolesData.value || [];

    // For each role, fetch members
    const results = [];
    for (const role of roles) {
        try {
            const membersRes = await fetch(
                `https://graph.microsoft.com/v1.0/directoryRoles/${role.id}/members` +
                `?$select=id,displayName,mail,userPrincipalName&$top=50`,
                { headers: { Authorization: `Bearer ${token}` } }
            );
            const membersData = membersRes.ok ? await membersRes.json() : { value: [] };
            results.push({
                ...role,
                members: (membersData.value || []).filter(m => m.userPrincipalName),
            });
        } catch {
            results.push({ ...role, members: [] });
        }
    }
    return results;
}

/**
 * Add a user to a group.
 * Requires: GroupMember.ReadWrite.All
 */
async function addGroupMember(groupId, userId) {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/groups/${groupId}/members/$ref`,
        {
            method: 'POST',
            headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ '@odata.id': `https://graph.microsoft.com/v1.0/directoryObjects/${userId}` }),
        }
    );
    if (!res.ok && res.status !== 204) {
        const err = await res.text();
        throw new Error(`Gruppe hinzufügen fehlgeschlagen: GroupMember.ReadWrite.All erforderlich. ${err}`);
    }
}

/**
 * Remove a user from a group.
 * Requires: GroupMember.ReadWrite.All
 */
async function removeGroupMember(groupId, userId) {
    if (!process.env.AZURE_TENANT_ID) throw new Error('Azure-Konfiguration fehlt');
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/groups/${groupId}/members/${userId}/$ref`,
        { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok && res.status !== 204) {
        const err = await res.text();
        throw new Error(`Gruppe entfernen fehlgeschlagen: GroupMember.ReadWrite.All erforderlich. ${err}`);
    }
}

/**
 * Fetch all Teams and their channels.
 * Requires: Team.ReadBasic.All, ChannelSettings.Read.All (Application Level)
 */
async function getTeamsAllChannels() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }

    const token = await getClientToken();
    const teams = [];
    let url = 'https://graph.microsoft.com/v1.0/teams?$select=id,displayName&$top=100';

    while (url) {
        const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
        if (!res.ok) throw new Error(`Teams-Abfrage fehlgeschlagen: ${await res.text()}`);
        const data = await res.json();
        teams.push(...(data.value || []));
        url = data['@odata.nextLink'] || null;
    }

    const result = [];
    for (const team of teams) {
        try {
            const res = await fetch(
                `https://graph.microsoft.com/v1.0/teams/${team.id}/channels?$select=id,displayName,createdDateTime`,
                { headers: { Authorization: `Bearer ${token}` } }
            );
            if (!res.ok) continue;
            const data = await res.json();
            for (const ch of (data.value || [])) {
                result.push({
                    teamId: team.id,
                    teamName: team.displayName,
                    channelId: ch.id,
                    channelName: ch.displayName,
                    createdDateTime: ch.createdDateTime || null,
                });
            }
        } catch { /* einzelne Teams überspringen */ }
    }
    return result;
}

/**
 * Polls Microsoft Defender for Office 365 alerts and persists new ones.
 * Requires: SecurityAlert.Read.All (Application permission with admin consent)
 */
async function pollMdoAlerts() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) return;

    try {
        const token = await getClientToken();
        const url =
            `https://graph.microsoft.com/v1.0/security/alerts_v2` +
            `?$filter=serviceSource eq 'microsoftDefenderForOffice365' and status ne 'resolved'` +
            `&$orderby=createdDateTime desc&$top=50`;

        const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
        if (!res.ok) {
            const err = await res.text();
            console.error('[MDO] Failed to fetch alerts:', err);
            return;
        }

        const alerts = (await res.json()).value || [];
        if (alerts.length === 0) return;

        console.log(`[MDO] Processing ${alerts.length} alert(s)`);

        for (const alert of alerts) {
            try {
                if (ExternalAlert.getByEmailMessageId(alert.id)) continue;

                const sevMap = { high: 'CRIT', medium: 'WARN', low: 'WARN', informational: 'UNKNOWN' };
                const severity = sevMap[(alert.severity || '').toLowerCase()] || 'UNKNOWN';
                const affectedUser =
                    (alert.userStates && alert.userStates[0]?.userPrincipalName) ||
                    alert.userPrincipalName || '';

                const created = ExternalAlert.create({
                    source:           'mdo',
                    device:           affectedUser,
                    service:          alert.category || '',
                    state_transition: alert.status || 'newAlert',
                    severity,
                    message:          alert.title || '',
                    customer:         '',
                    monitored_by:     'Microsoft Defender for Office 365',
                    state_time:       alert.createdDateTime || '',
                    raw_body:         JSON.stringify(alert),
                    email_message_id: alert.id,
                });

                console.log(`[MDO] Alert gespeichert: ${alert.title} (${severity}) – ${affectedUser}`);

                if (created && AiService.isConfigured()) {
                    AiService.analyzeMdoAlert(created).then(analysis => {
                        if (analysis) ExternalAlert.setAiAnalysis(created.id, analysis);
                    }).catch(() => {});
                }
            } catch (alertErr) {
                console.error('[MDO] Error processing alert:', alertErr.message);
            }
        }
    } catch (err) {
        console.error('[MDO] Poll error:', err.message);
    }
}

/**
 * Get detailed Entra ID profile for a single user.
 * Requires: User.Read.All
 */
async function getUserEntraProfile(azureId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(azureId)}` +
        `?$select=id,userPrincipalName,onPremisesSamAccountName,onPremisesDistinguishedName,` +
        `onPremisesSyncEnabled,userType,accountEnabled,onPremisesLastSyncDateTime,` +
        `displayName,givenName,surname,mail,jobTitle,department,officeLocation,` +
        `mobilePhone,businessPhones,createdDateTime,signInSessionsValidFromDateTime`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Entra-Profil-Abfrage fehlgeschlagen: ${err}`);
    }
    return await res.json();
}

/**
 * Get license details assigned to a specific user.
 * Requires: User.Read.All
 */
async function getUserLicenseDetails(azureId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(azureId)}/licenseDetails`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Lizenz-Abfrage fehlgeschlagen: ${err}`);
    }
    const data = await res.json();
    return data.value || [];
}

/**
 * Get recent sign-in activity for a user.
 * Requires: AuditLog.Read.All
 */
async function getUserSignInActivity(azureId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    const token = await getClientToken();
    const encoded = encodeURIComponent(`userId eq '${azureId}'`);
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=${encoded}&$top=5&$orderby=createdDateTime desc`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) return [];
    const data = await res.json();
    return data.value || [];
}

/**
 * Get sign-in risk for a user from Identity Protection.
 * Requires: IdentityRiskyUser.Read.All
 */
async function getUserSignInRisk(azureId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    const token = await getClientToken();
    const encoded = encodeURIComponent(`id eq '${azureId}'`);
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?$filter=${encoded}`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) return null;
    const data = await res.json();
    return (data.value || [])[0] || null;
}

/**
 * Get all conditional access policies for the tenant.
 * Requires: Policy.Read.All
 */
async function getConditionalAccessPolicies() {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Conditional-Access-Abfrage fehlgeschlagen: ${err}`);
    }
    const data = await res.json();
    return data.value || [];
}

/**
 * Invalidate all refresh tokens for a user (forces re-authentication via Entra).
 * Requires: User.ReadWrite.All
 */
async function invalidateUserSessions(azureId) {
    if (!process.env.AZURE_TENANT_ID || !process.env.AZURE_CLIENT_ID || !process.env.AZURE_CLIENT_SECRET) {
        throw new Error('Azure-Konfiguration fehlt');
    }
    const token = await getClientToken();
    const res = await fetch(
        `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(azureId)}/invalidateAllRefreshTokens`,
        { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Length': '0' } }
    );
    if (!res.ok) {
        const err = await res.text();
        throw new Error(`Session-Invalidierung fehlgeschlagen: ${err}`);
    }
    return true;
}

module.exports = { getClientToken, pollMailbox, pollMonitoringMailbox, pollMdoAlerts, getTeamsAllChannels, getIntuneManagedDevices, getAzureGroups, getAzureGroupMembers, getEntraSubscribedSkus, getEntraLicenseUsers, getDepartmentManager, searchEntraUserByName, syncItTopicsToPlanner, getPlannerBuckets, getSecureScore, getRiskyUsers, getDefenderAlerts, getNonCompliantDevices, getUsersWithoutMfa, getEntraUsers, getAllMfaRegistrationDetails, getEntraUserGroups, getDirectoryRolesWithMembers, addGroupMember, removeGroupMember, getUserEntraProfile, getUserLicenseDetails, getUserSignInActivity, getUserSignInRisk, getConditionalAccessPolicies, invalidateUserSessions };
