import { isAllowedAuthorityHost, isAllowedGraphHost } from '../data/graphHosts.js';

function sanitizeBase(url = '') {
  return String(url || '').replace(/\/+$/, '');
}

// Defense in depth: even though connection URLs are validated on write, re-check
// before every outbound request so stored/legacy rows can't trigger SSRF.
function assertAllowedEndpoints(connection) {
  if (!isAllowedAuthorityHost(connection.authority_host)) {
    throw new Error('Graph connection authority host is not an allowed Microsoft endpoint');
  }
  if (!isAllowedGraphHost(connection.graph_base_url)) {
    throw new Error('Graph connection base URL is not an allowed Microsoft endpoint');
  }
}

export async function getGraphToken(connection) {
  assertAllowedEndpoints(connection);
  const authority = sanitizeBase(connection.authority_host);
  const graphBase = sanitizeBase(connection.graph_base_url);
  const response = await fetch(`${authority}/${encodeURIComponent(connection.tenant_id)}/oauth2/v2.0/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: connection.client_id,
      client_secret: connection.clientSecret,
      scope: `${graphBase}/.default`,
      grant_type: 'client_credentials'
    })
  });
  const payload = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(payload.error_description || payload.error || `Token request failed: ${response.status}`);
  return payload.access_token;
}

export async function graphRequest(connection, path, options = {}) {
  assertAllowedEndpoints(connection);
  const token = options.token || await getGraphToken(connection);
  const base = sanitizeBase(connection.graph_base_url);
  const response = await fetch(`${base}${path}`, {
    method: options.method || 'GET',
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
      ...(options.body ? { 'Content-Type': 'application/json' } : {})
    },
    body: options.body ? JSON.stringify(options.body) : undefined
  });
  const text = await response.text();
  const payload = text ? JSON.parse(text) : {};
  if (!response.ok) {
    const message = payload?.error?.message || payload?.error_description || `Graph request failed: ${response.status}`;
    const error = new Error(message);
    error.status = response.status;
    error.payload = payload;
    throw error;
  }
  return payload;
}

export async function testGraphConnection(connection) {
  const token = await getGraphToken(connection);
  const payload = await graphRequest(connection, '/v1.0/deviceAppManagement?$select=id&$top=1', { token });
  return { ok: true, message: `Connected. deviceAppManagement readable; sample keys: ${Object.keys(payload).join(', ')}` };
}

// Create or update the win32LobApp metadata object. Content (.intunewin bytes)
// upload is a separate Phase 2 concern; this writes the app definition only.
// mobileApps create/update lives on the beta endpoint for full win32 fidelity.
export async function upsertWin32App(connection, payload, existingAppId = '') {
  const token = await getGraphToken(connection);
  if (existingAppId) {
    await graphRequest(connection, `/beta/deviceAppManagement/mobileApps/${encodeURIComponent(existingAppId)}`, {
      token,
      method: 'PATCH',
      body: payload
    });
    return { id: existingAppId, updated: true };
  }
  const created = await graphRequest(connection, '/beta/deviceAppManagement/mobileApps', {
    token,
    method: 'POST',
    body: payload
  });
  return { id: created.id, created: true, raw: created };
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function waitForFileState(connection, token, appPath, cvId, fileId, target, { attempts = 30, delayMs = 2000 } = {}) {
  for (let i = 0; i < attempts; i++) {
    const file = await graphRequest(connection, `${appPath}/contentVersions/${cvId}/files/${fileId}`, { token });
    const state = file.uploadState || '';
    if (state === target) return file;
    if (state.endsWith('Failed')) throw new Error(`Intune content upload failed in state "${state}"`);
    await sleep(delayMs);
  }
  throw new Error(`Timed out waiting for Intune content state "${target}"`);
}

// Upload encrypted .intunewin content to Azure Storage in blocks, then commit.
// The SAS URI is returned by Graph (trusted); the block PUTs go straight to
// Azure Blob storage, outside the Graph allow-list.
async function azureBlockUpload(sasUri, content) {
  const blockSize = 6 * 1024 * 1024;
  const blockIds = [];
  let index = 0;
  for (let start = 0; start < content.length; start += blockSize) {
    const chunk = content.subarray(start, start + blockSize);
    const blockId = Buffer.from(String(index).padStart(8, '0')).toString('base64');
    blockIds.push(blockId);
    const res = await fetch(`${sasUri}&comp=block&blockid=${encodeURIComponent(blockId)}`, {
      method: 'PUT',
      headers: { 'x-ms-blob-type': 'BlockBlob' },
      body: chunk
    });
    if (!res.ok) throw new Error(`Azure block upload failed (${res.status}) on block ${index}`);
    index += 1;
  }
  const listXml = `<?xml version="1.0" encoding="utf-8"?><BlockList>${blockIds.map((b) => `<Latest>${b}</Latest>`).join('')}</BlockList>`;
  const commit = await fetch(`${sasUri}&comp=blocklist`, {
    method: 'PUT',
    headers: { 'Content-Type': 'text/plain; charset=UTF-8' },
    body: listXml
  });
  if (!commit.ok) throw new Error(`Azure block-list commit failed (${commit.status})`);
}

// Full Win32 content commit flow (Phase 2). Requires a live tenant + Azure
// Storage; validated end to end only against a real tenant. `parsed` is the
// output of parseIntunewin().
export async function uploadWin32Content(connection, appId, parsed) {
  const token = await getGraphToken(connection);
  const appPath = `/beta/deviceAppManagement/mobileApps/${encodeURIComponent(appId)}/microsoft.graph.win32LobApp`;

  const version = await graphRequest(connection, `${appPath}/contentVersions`, { token, method: 'POST', body: {} });
  const cvId = version.id;

  const fileMeta = await graphRequest(connection, `${appPath}/contentVersions/${cvId}/files`, {
    token,
    method: 'POST',
    body: {
      '@odata.type': '#microsoft.graph.mobileAppContentFile',
      name: parsed.setupFile || 'IntunePackage.intunewin',
      size: parsed.unencryptedContentSize,
      sizeEncrypted: parsed.encryptedContentSize,
      isDependency: false,
      manifest: null
    }
  });
  const fileId = fileMeta.id;

  const ready = await waitForFileState(connection, token, appPath, cvId, fileId, 'azureStorageUriRequestSuccess');
  await azureBlockUpload(ready.azureStorageUri, parsed.encryptedContent);

  await graphRequest(connection, `${appPath}/contentVersions/${cvId}/files/${fileId}/commit`, {
    token,
    method: 'POST',
    body: { fileEncryptionInfo: parsed.fileEncryptionInfo }
  });
  await waitForFileState(connection, token, appPath, cvId, fileId, 'commitFileSuccess');

  await graphRequest(connection, `/beta/deviceAppManagement/mobileApps/${encodeURIComponent(appId)}`, {
    token,
    method: 'PATCH',
    body: { '@odata.type': '#microsoft.graph.win32LobApp', committedContentVersion: cvId }
  });

  return { contentVersionId: cvId, fileId, size: parsed.unencryptedContentSize, sizeEncrypted: parsed.encryptedContentSize };
}

// Replace the app's assignments in one call. The /assign action is a full
// replacement, so callers send the complete desired assignment set.
export async function assignWin32App(connection, appId, mobileAppAssignments) {
  const token = await getGraphToken(connection);
  await graphRequest(connection, `/beta/deviceAppManagement/mobileApps/${encodeURIComponent(appId)}/assign`, {
    token,
    method: 'POST',
    body: { mobileAppAssignments }
  });
  return { ok: true, count: mobileAppAssignments.length };
}

// Replace the app's supersedence/dependency relationships. updateRelationships
// is also a full replacement of the app's outgoing relationship set.
export async function setWin32AppRelationships(connection, appId, relationships) {
  const token = await getGraphToken(connection);
  await graphRequest(connection, `/beta/deviceAppManagement/mobileApps/${encodeURIComponent(appId)}/updateRelationships`, {
    token,
    method: 'POST',
    body: { relationships }
  });
  return { ok: true, count: relationships.length };
}

// Live win32LobApp metadata + assignments + relationships, used by drift checks.
export async function getWin32AppState(connection, appId) {
  const token = await getGraphToken(connection);
  const base = `/beta/deviceAppManagement/mobileApps/${encodeURIComponent(appId)}`;
  const [app, assignments, relationships] = await Promise.all([
    graphRequest(connection, base, { token }),
    graphRequest(connection, `${base}/assignments`, { token }).then((r) => r.value || []).catch(() => []),
    graphRequest(connection, `${base}/relationships`, { token }).then((r) => r.value || []).catch(() => [])
  ]);
  return { app, assignments, relationships };
}

export async function getGraphMobileApp(connection, appId) {
  const token = await getGraphToken(connection);
  const version = connection.default_api_version || 'v1.0';
  return graphRequest(
    connection,
    `/${version}/deviceAppManagement/mobileApps/${encodeURIComponent(appId)}?$select=id,displayName,publisher,displayVersion`,
    { token }
  );
}

export async function listGraphMobileApps(connection, query = '') {
  const token = await getGraphToken(connection);
  const version = connection.default_api_version || 'v1.0';
  const filter = query ? `&$filter=contains(displayName,'${String(query).replaceAll("'", "''")}')` : '';
  const payload = await graphRequest(
    connection,
    `/${version}/deviceAppManagement/mobileApps?$select=id,displayName,publisher,createdDateTime,lastModifiedDateTime,isAssigned${filter}&$top=50`,
    { token }
  );
  return (payload.value || []).map((app) => ({
    id: app.id,
    displayName: app.displayName || app.name || app.id,
    publisher: app.publisher || '',
    createdDateTime: app.createdDateTime || '',
    lastModifiedDateTime: app.lastModifiedDateTime || '',
    isAssigned: Boolean(app.isAssigned)
  }));
}

export async function syncIntuneDeploymentStatus(connection, graphAppId, mode = 'reports') {
  const token = await getGraphToken(connection);
  const rows = [];
  const sources = [];
  let overview = {};

  if (mode === 'reports' || mode === 'both') {
    const reportResult = await syncReports(connection, token, graphAppId);
    rows.push(...reportResult.rows);
    overview = reportResult.overview;
    sources.push(...reportResult.sources);
  }

  if ((mode === 'legacy-status' || mode === 'both') && !rows.length) {
    const legacyRows = await syncLegacyDeviceStatuses(connection, token, graphAppId);
    rows.push(...legacyRows);
    sources.push('legacy-deviceStatuses');
  }

  return {
    rows,
    overview,
    sources,
    summary: summarizeRows(rows, overview)
  };
}

async function syncReports(connection, token, graphAppId) {
  const rows = [];
  const sources = [];
  let overview = {};
  try {
    const appOverview = await graphRequest(connection, '/beta/deviceManagement/reports/getAppStatusOverviewReport', {
      token,
      method: 'POST',
      body: { filter: `(ApplicationId eq '${graphAppId}')` }
    });
    overview = reportRows(appOverview)[0] || {};
    sources.push('getAppStatusOverviewReport');
  } catch (error) {
    overview = { warning: `Overview report unavailable: ${error.message}` };
  }

  try {
    const deviceReport = await graphRequest(connection, '/beta/deviceManagement/reports/getDeviceInstallStatusReport', {
      token,
      method: 'POST',
      body: {
        select: [
          'DeviceId',
          'DeviceName',
          'UserPrincipalName',
          'Platform',
          'Status',
          'ErrorCode',
          'AppInstallState',
          'InstallState',
          'InstallStateDetail',
          'LastModifiedDateTime',
          'ApplicationId'
        ],
        filter: `(ApplicationId eq '${graphAppId}')`,
        skip: 0,
        top: 50000,
        orderBy: []
      }
    });
    rows.push(...reportRows(deviceReport).map((row) => normalizeReportStatus(row, 'device')));
    sources.push('getDeviceInstallStatusReport');
  } catch (error) {
    rows.push(failureRow('device', 'report-unavailable', error.message, { source: 'getDeviceInstallStatusReport' }));
  }

  try {
    const userReport = await graphRequest(connection, '/beta/deviceManagement/reports/getUserInstallStatusReport', {
      token,
      method: 'POST',
      body: {
        select: ['UserId', 'UserName', 'UserPrincipalName', 'Status', 'InstalledDeviceCount', 'FailedDeviceCount', 'ApplicationId'],
        filter: `(ApplicationId eq '${graphAppId}')`,
        skip: 0,
        top: 50000,
        orderBy: []
      }
    });
    rows.push(...reportRows(userReport).map((row) => normalizeReportStatus(row, 'user')));
    sources.push('getUserInstallStatusReport');
  } catch (error) {
    rows.push(failureRow('user', 'report-unavailable', error.message, { source: 'getUserInstallStatusReport' }));
  }

  return { rows, overview, sources };
}

async function syncLegacyDeviceStatuses(connection, token, graphAppId) {
  const payload = await graphRequest(connection, `/beta/deviceAppManagement/mobileApps/${graphAppId}/deviceStatuses`, { token });
  return (payload.value || []).map((row) => ({
    scope: 'device',
    principalId: row.id || '',
    principalName: row.deviceName || row.userName || '',
    deviceId: row.deviceId || '',
    deviceName: row.deviceName || '',
    userId: row.userId || '',
    userPrincipalName: row.userPrincipalName || row.userName || '',
    installState: normalizeState(row.installState || row.mobileAppInstallStatusValue || row.status),
    installStateDetail: row.installStateDetail || '',
    errorCode: row.errorCode,
    errorDescription: row.errorDescription || failureCause(row.errorCode, row.installStateDetail),
    lastSyncDateTime: row.lastSyncDateTime || '',
    raw: row
  }));
}

function reportRows(report) {
  const schema = report.Schema || report.schema || [];
  const values = report.Values || report.values || report.value || [];
  if (!Array.isArray(values)) return [];
  if (!schema.length) return values;
  return values.map((entry) => {
    if (!Array.isArray(entry)) return entry;
    return Object.fromEntries(schema.map((column, index) => [column.Column || column.column || column.Name || column.name, entry[index]]));
  });
}

function normalizeReportStatus(row, scope) {
  const installState = normalizeState(row.Status || row.InstallState || row.AppInstallState || row.State || row.InstallStatus);
  const errorCode = row.ErrorCode ?? row.Error ?? row.HexErrorCode ?? '';
  return {
    scope,
    principalId: scope === 'device' ? row.DeviceId || row.ManagedDeviceId || '' : row.UserId || '',
    principalName: scope === 'device' ? row.DeviceName || '' : row.UserName || row.UserPrincipalName || '',
    deviceId: row.DeviceId || row.ManagedDeviceId || '',
    deviceName: row.DeviceName || '',
    userId: row.UserId || '',
    userPrincipalName: row.UserPrincipalName || row.UserName || '',
    installState,
    installStateDetail: row.InstallStateDetail || row.StatusDetails || row.StateDetails || '',
    errorCode,
    errorDescription: row.ErrorDescription || row.ErrorDetails || failureCause(errorCode, row.InstallStateDetail),
    lastSyncDateTime: row.LastModifiedDateTime || row.LastSyncDateTime || row.LastReportedDateTime || '',
    raw: row
  };
}

function normalizeState(value = '') {
  const text = String(value || '').toLowerCase();
  if (text.includes('fail') || text === 'error') return 'failed';
  if (text.includes('install') || text === 'success' || text === 'installed') return 'installed';
  if (text.includes('pending') || text.includes('progress')) return 'pending';
  if (text.includes('notapplicable') || text.includes('not applicable')) return 'notApplicable';
  if (text.includes('notinstalled') || text.includes('not installed')) return 'notInstalled';
  return text || 'unknown';
}

function failureRow(scope, code, message, raw) {
  return {
    scope,
    installState: 'failed',
    installStateDetail: code,
    errorCode: code,
    errorDescription: message,
    raw
  };
}

function failureCause(errorCode, detail = '') {
  const code = String(errorCode || '').toLowerCase();
  const known = {
    '0x87d1041c': 'Intune did not detect the app after installation. Check detection rules, install command, and app install timing.',
    '0x87d13b9f': 'App installation failed. Review Intune Management Extension logs and PSADT logs.',
    '1602': 'User deferred or cancelled installation. Confirm return code is mapped as retry and DeferRunInterval is configured when needed.',
    '3010': 'Soft reboot required. Confirm Intune return code behavior.',
    '1703': 'Legacy soft reboot mapping. Confirm this is intentional for the package.'
  };
  return known[code] || detail || '';
}

function summarizeRows(rows, overview = {}) {
  const counts = { total: rows.length, installed: 0, failed: 0, pending: 0, notInstalled: 0, notApplicable: 0, unknown: 0 };
  for (const row of rows) {
    const state = row.installState || 'unknown';
    if (counts[state] == null) counts.unknown += 1;
    else counts[state] += 1;
  }
  return { counts, overview };
}
