import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { Client } from '@microsoft/microsoft-graph-client';
import { ClientSecretCredential } from '@azure/identity';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { DocAuthorityLevel, DocLifecycleStatus, DocType } from '@prisma/client';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as path from 'path';
import { Readable, Transform } from 'stream';
import { pipeline } from 'stream/promises';

interface DriveItem {
  id: string;
  name: string;
  webUrl: string;
  eTag?: string;
  size?: number;
  file?: {
    mimeType?: string;
  };
  folder?: {
    childCount?: number;
  };
  createdDateTime?: string;
  lastModifiedDateTime?: string;
  createdBy?: {
    user?: {
      displayName?: string;
    };
  };
  lastModifiedBy?: {
    user?: {
      displayName?: string;
    };
  };
  parentReference?: {
    driveId?: string;
    siteId?: string;
    path?: string;
  };
  listItem?: {
    fields?: {
      DocAuthorityLevel?: string;
      DocLifecycleStatus?: string;
      DocType?: string;
    };
  };
}

interface DeltaDriveItem extends DriveItem {
  deleted?: {
    state?: string;
  };
  removed?: {
    reason?: string;
  };
  '@removed'?: {
    reason?: string;
  };
}

type SharePointDownloadResult =
  | { status: 'downloaded'; filePath: string; hash: string; contentType: string | null }
  | { status: 'not_modified' };

export interface SyncedSharePointDocument {
  item: DriveItem;
  record: {
    id: string;
    title: string;
    webUrl: string;
    fileExtension: string | null;
    docType: DocType;
    docAuthorityLevel: DocAuthorityLevel;
    spModifiedAt: Date | null;
    createdBy: string | null;
    spEtag: string | null;
    size: bigint | null;
  };
  previousMetadata: {
    spModifiedAt: Date | null;
    spEtag: string | null;
    size: bigint | null;
  } | null;
}

@Injectable()
export class SharePointSyncService {
  private readonly logger = new Logger(SharePointSyncService.name);
  private client: Client | null = null;
  private siteId: string | null = null;
  private driveId: string | null = null;

  constructor(
    private readonly configService: ConfigService,
    private readonly prisma: PrismaService,
  ) {}

  /**
   * 全量同步元数据（SharePoint -> SPDocumentIndex）
   */
  async syncAllMetadata(): Promise<{
    driveId: string;
    documents: SyncedSharePointDocument[];
    cursors: Array<{ scopePath: string; deltaLink: string }>;
  }> {
    const client = this.getClient();
    const driveId = await this.getDriveId(client);
    const siteId = await this.getSiteId(client);
    const includedPaths = this.getIncludedPaths();
    const scopePaths = includedPaths.length > 0 ? includedPaths : ['/'];

    this.logger.log(`Syncing SharePoint metadata for drive: ${driveId}`);
    if (includedPaths.length > 0) {
      this.logger.log(`SharePoint sync scope limited to paths: ${includedPaths.join(', ')}`);
    }

    const documents: SyncedSharePointDocument[] = [];
    const currentFolderIds: string[] = [];
    const cursors: Array<{ scopePath: string; deltaLink: string }> = [];

    for (const scopePath of scopePaths) {
      const { items, deltaLink } = await this.fetchDeltaItems(client, driveId, scopePath, {
        forceStart: true,
      });
      if (deltaLink) {
        cursors.push({ scopePath, deltaLink });
      }
      for (const item of items) {
        if (this.isRemovedDeltaItem(item)) {
          continue;
        }
        if (item.file) {
          const itemPath = this.getItemPath(item);
          if (!this.shouldIncludeFile(itemPath, includedPaths)) {
            continue;
          }
          const result = await this.syncFileItem(driveId, siteId, item);
          if (result) {
            documents.push({ item, record: result.record, previousMetadata: result.previousMetadata });
          }
          continue;
        }
        if (item.folder) {
          const folderPath = this.getItemPath(item);
          if (!this.shouldTraverseFolder(folderPath, includedPaths)) {
            continue;
          }
          const folderRecordId = await this.syncFolderItem(driveId, siteId, item, folderPath);
          if (folderRecordId) {
            currentFolderIds.push(folderRecordId);
          }
        }
      }
    }

    await this.cleanupMissingDocuments(documents.map((doc) => doc.record.id));
    await this.cleanupMissingFolders(currentFolderIds);
    return { driveId, documents, cursors };
  }

  /**
   * 增量同步元数据（SharePoint Delta -> SPDocumentIndex）
   */
  async syncDeltaMetadata(): Promise<{
    driveId: string;
    documents: SyncedSharePointDocument[];
    removedItemIds: string[];
    cursors: Array<{ scopePath: string; deltaLink: string }>;
  }> {
    const client = this.getClient();
    const driveId = await this.getDriveId(client);
    const siteId = await this.getSiteId(client);
    const includedPaths = this.getIncludedPaths();
    const scopePaths = includedPaths.length > 0 ? includedPaths : ['/'];

    this.logger.log(`Syncing SharePoint delta metadata for drive: ${driveId}`);
    if (includedPaths.length > 0) {
      this.logger.log(`SharePoint delta sync scope limited to paths: ${includedPaths.join(', ')}`);
    }

    const documents: SyncedSharePointDocument[] = [];
    const removedItemIds: string[] = [];
    const cursors: Array<{ scopePath: string; deltaLink: string }> = [];

    for (const scopePath of scopePaths) {
      const { items, deltaLink } = await this.fetchDeltaItems(client, driveId, scopePath);
      if (deltaLink) {
        cursors.push({ scopePath, deltaLink });
      }

      for (const item of items) {
        if (this.isRemovedDeltaItem(item)) {
          removedItemIds.push(item.id);
          continue;
        }
        if (item.file) {
          const itemPath = this.getItemPath(item);
          if (!this.shouldIncludeFile(itemPath, includedPaths)) {
            continue;
          }
          const result = await this.syncFileItem(driveId, siteId, item);
          if (result) {
            documents.push({ item, record: result.record, previousMetadata: result.previousMetadata });
          }
          continue;
        }
        if (item.folder) {
          const folderPath = this.getItemPath(item);
          if (!this.shouldTraverseFolder(folderPath, includedPaths)) {
            continue;
          }
          await this.syncFolderItem(driveId, siteId, item, folderPath);
        }
      }
    }

    await this.cleanupRemovedFolders(removedItemIds, driveId);
    return { driveId, documents, removedItemIds, cursors };
  }

  async shouldFallbackToFullSyncForDelta(): Promise<{ shouldFallback: boolean; reason?: string }> {
    try {
      const client = this.getClient();
      const driveId = await this.getDriveId(client);
      const includedPaths = this.getIncludedPaths();
      const scopePaths = includedPaths.length > 0 ? includedPaths : ['/'];

      const cursorCount = await this.prisma.sharePointSyncCursor.count({
        where: {
          driveId,
          scopePath: { in: scopePaths },
        },
      });

      if (cursorCount < scopePaths.length) {
        return {
          shouldFallback: true,
          reason: 'delta cursor missing',
        };
      }
      return { shouldFallback: false };
    } catch (error: any) {
      return {
        shouldFallback: true,
        reason: error?.message || 'delta cursor unavailable',
      };
    }
  }

  async saveDeltaCursors(
    driveId: string,
    cursors: Array<{ scopePath: string; deltaLink: string }>,
  ): Promise<void> {
    if (cursors.length === 0) {
      return;
    }

    const now = new Date();
    for (const cursor of cursors) {
      await this.prisma.sharePointSyncCursor.upsert({
        where: {
          driveId_scopePath: {
            driveId,
            scopePath: cursor.scopePath,
          },
        },
        create: {
          driveId,
          scopePath: cursor.scopePath,
          deltaLink: cursor.deltaLink,
          lastSyncedAt: now,
        },
        update: {
          deltaLink: cursor.deltaLink,
          lastSyncedAt: now,
        },
      });
    }
  }

  async downloadItemToTemp(
    driveId: string,
    itemId: string,
    filename: string,
    expectedSize?: number,
    options?: { ifNoneMatch?: string | null; ifModifiedSince?: Date | null },
  ): Promise<SharePointDownloadResult> {
    const client = this.getClient();
    const item = await client.api(`/drives/${driveId}/items/${itemId}`).get();
    const downloadUrl = item?.['@microsoft.graph.downloadUrl'];
    if (!downloadUrl) {
      throw new Error(`Missing download URL for item ${itemId}`);
    }

    const safeName = filename.replace(/[\\/:*?"<>|]+/g, '_');
    const tempDir = path.join(process.cwd(), 'tmp', 'knowledge-base');
    await fs.promises.mkdir(tempDir, { recursive: true });
    const filePath = path.join(tempDir, `${Date.now()}-${safeName}`);
    let lastError: Error | null = null;
    const ifNoneMatch = options?.ifNoneMatch ?? null;
    const ifModifiedSince = options?.ifModifiedSince ?? null;

    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        const result = await this.downloadFromUrl(
          downloadUrl,
          filePath,
          `SharePoint item ${itemId}`,
          { ifNoneMatch, ifModifiedSince },
        );
        if (result.status === 'not_modified') {
          return result;
        }
        await this.validateDownloadedFile(filePath, filename, expectedSize, result.contentType);
        return result;
      } catch (error: any) {
        lastError = error instanceof Error ? error : new Error(String(error));
        this.logger.warn(
          `SharePoint downloadUrl attempt ${attempt + 1} failed for ${filename}: ${lastError.message}`,
        );
        await this.safeCleanup(filePath);
        const delayMs = 400 * Math.pow(2, attempt);
        await new Promise((resolve) => setTimeout(resolve, delayMs));
      }
    }

    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        const result = await this.downloadFromGraph(client, driveId, itemId, filePath, {
          ifNoneMatch,
          ifModifiedSince,
        });
        if (result.status === 'not_modified') {
          return result;
        }
        await this.validateDownloadedFile(filePath, filename, expectedSize, null);
        return result;
      } catch (error: any) {
        lastError = error instanceof Error ? error : new Error(String(error));
        this.logger.warn(
          `SharePoint download fallback attempt ${attempt + 1} failed for ${filename}: ${lastError.message}`,
        );
        await this.safeCleanup(filePath);
        const delayMs = 600 * Math.pow(2, attempt);
        await new Promise((resolve) => setTimeout(resolve, delayMs));
      }
    }

    const message = lastError?.message || 'SharePoint download failed';
    throw new Error(`SharePoint download fallback failed: ${message}`);
  }

  private async downloadFromUrl(
    url: string,
    filePath: string,
    label: string,
    options?: { ifNoneMatch?: string | null; ifModifiedSince?: Date | null },
  ): Promise<SharePointDownloadResult> {
    const headers: Record<string, string> = {};
    if (options?.ifNoneMatch) {
      headers['If-None-Match'] = options.ifNoneMatch;
    }
    if (options?.ifModifiedSince) {
      headers['If-Modified-Since'] = options.ifModifiedSince.toUTCString();
    }
    const response = await fetch(url, { headers });
    if (response.status === 304) {
      return { status: 'not_modified' };
    }
    if (!response.ok) {
      throw new Error(`${label} download failed (${response.status})`);
    }
    const contentType = response.headers.get('content-type');
    if (contentType && (contentType.includes('text/html') || contentType.includes('application/json'))) {
      throw new Error(`SharePoint download returned non-binary content (${contentType})`);
    }

    const body = response.body;
    if (!body) {
      throw new Error('SharePoint download stream is unavailable');
    }

    const { hash } = await this.writeStreamToFile(Readable.fromWeb(body as any), filePath);
    return { status: 'downloaded', filePath, hash, contentType: contentType ?? null };
  }

  private async downloadFromGraph(
    client: Client,
    driveId: string,
    itemId: string,
    filePath: string,
    options?: { ifNoneMatch?: string | null; ifModifiedSince?: Date | null },
  ): Promise<SharePointDownloadResult> {
    try {
      let request = client.api(`/drives/${driveId}/items/${itemId}/content`);
      if (options?.ifNoneMatch) {
        request = request.header('If-None-Match', options.ifNoneMatch);
      }
      if (options?.ifModifiedSince) {
        request = request.header('If-Modified-Since', options.ifModifiedSince.toUTCString());
      }
      const stream = (await request.getStream()) as NodeJS.ReadableStream;
      if (!stream) {
        throw new Error('SharePoint content stream is unavailable');
      }
      const { hash } = await this.writeStreamToFile(stream, filePath);
      return { status: 'downloaded', filePath, hash, contentType: null };
    } catch (error: any) {
      if (error?.statusCode === 304) {
        return { status: 'not_modified' };
      }
      throw error;
    }
  }

  private async writeStreamToFile(
    stream: NodeJS.ReadableStream,
    filePath: string,
  ): Promise<{ hash: string }> {
    const fileStream = fs.createWriteStream(filePath, { flags: 'w' });
    const hash = crypto.createHash('sha256');
    const hashStream = new Transform({
      transform(chunk, _encoding, callback) {
        hash.update(chunk as Buffer);
        callback(null, chunk);
      },
    });
    await pipeline(stream, hashStream, fileStream);
    return { hash: hash.digest('hex') };
  }

  private async validateDownloadedFile(
    filePath: string,
    filename: string,
    expectedSize?: number,
    contentType?: string | null,
  ): Promise<void> {
    const fileStat = await fs.promises.stat(filePath);
    if (!fileStat.size) {
      throw new Error('SharePoint download resulted in empty file');
    }
    if (expectedSize && Number(expectedSize) !== Number(fileStat.size)) {
      this.logger.warn(
        `SharePoint download size mismatch for ${filename}: expected ${expectedSize}, got ${fileStat.size}`,
      );
    }
    if (contentType && (contentType.includes('text/html') || contentType.includes('application/json'))) {
      throw new Error(`SharePoint download returned non-binary content (${contentType})`);
    }

    const ext = path.extname(filename).toLowerCase();
    if (['.pptx', '.docx', '.xlsx'].includes(ext)) {
      const handle = await fs.promises.open(filePath, 'r');
      try {
        const header = Buffer.alloc(2);
        await handle.read(header, 0, 2, 0);
        if (header.toString('utf8') !== 'PK') {
          throw new Error(`SharePoint download signature mismatch for ${filename}`);
        }
      } finally {
        await handle.close();
      }
    }
  }

  private async safeCleanup(filePath: string): Promise<void> {
    try {
      await fs.promises.unlink(filePath);
    } catch {
      // ignore cleanup errors
    }
  }

  private async syncFileItem(
    driveId: string,
    siteId: string,
    item: DriveItem,
  ): Promise<{ record: SyncedSharePointDocument['record']; previousMetadata: SyncedSharePointDocument['previousMetadata'] } | null> {
    if (!item.file) {
      return null;
    }

    const fileExtension = item.name?.includes('.') ? item.name.split('.').pop()?.toLowerCase() : null;

    // 解析 SharePoint 自定义字段
    const fields = item.listItem?.fields ?? {};
    const docAuthorityLevel = this.parseDocAuthorityLevel(fields.DocAuthorityLevel);
    const docLifecycleStatus = this.parseDocLifecycleStatus(fields.DocLifecycleStatus);
    const docType = this.parseDocType(fields.DocType);

    const data = {
      spItemId: item.id,
      spDriveId: driveId,
      spSiteId: siteId,
      title: item.name,
      webUrl: item.webUrl,
      fileType: item.file?.mimeType ?? null,
      fileExtension,
      size: item.size ? BigInt(item.size) : null,
      spEtag: item.eTag ?? null,
      docAuthorityLevel,
      docLifecycleStatus,
      docType,
      createdBy: item.createdBy?.user?.displayName ?? null,
      lastModifiedBy: item.lastModifiedBy?.user?.displayName ?? null,
      spCreatedAt: item.createdDateTime ? new Date(item.createdDateTime) : null,
      spModifiedAt: item.lastModifiedDateTime ? new Date(item.lastModifiedDateTime) : null,
    };

    const existing = await this.prisma.sPDocumentIndex.findUnique({
      where: {
        spItemId_spDriveId: {
          spItemId: item.id,
          spDriveId: driveId,
        },
      },
      select: {
        spModifiedAt: true,
        spEtag: true,
        size: true,
      },
    });

    const record = await this.prisma.sPDocumentIndex.upsert({
      where: {
        spItemId_spDriveId: {
          spItemId: item.id,
          spDriveId: driveId,
        },
      },
      create: data,
      update: data,
    });

    this.logger.debug(`Synced document: ${item.name}`);
    return {
      record,
      previousMetadata: existing
        ? {
            spModifiedAt: existing.spModifiedAt ?? null,
            spEtag: existing.spEtag ?? null,
            size: existing.size ?? null,
          }
        : null,
    };
  }

  private async syncFolderItem(
    driveId: string,
    siteId: string,
    item: DriveItem,
    resolvedFolderPath?: string | null,
  ): Promise<string | null> {
    if (!item.folder) {
      return null;
    }

    const folderPath = resolvedFolderPath || this.getItemPath(item);
    if (!folderPath) {
      this.logger.warn(`Skip folder without resolvable path: ${item.name} (${item.id})`);
      return null;
    }

    const data = {
      spItemId: item.id,
      spDriveId: driveId,
      spSiteId: siteId,
      title: item.name,
      folderPath,
      webUrl: item.webUrl,
      createdBy: item.createdBy?.user?.displayName ?? null,
      lastModifiedBy: item.lastModifiedBy?.user?.displayName ?? null,
      spCreatedAt: item.createdDateTime ? new Date(item.createdDateTime) : null,
      spModifiedAt: item.lastModifiedDateTime ? new Date(item.lastModifiedDateTime) : null,
    };

    const record = await this.prisma.sPFolderIndex.upsert({
      where: {
        spItemId_spDriveId: {
          spItemId: item.id,
          spDriveId: driveId,
        },
      },
      create: data,
      update: data,
    });

    this.logger.debug(`Synced folder: ${folderPath}`);
    return record.id;
  }

  private async fetchAllDriveItems(
    client: Client,
    driveId: string,
    includedPaths: string[],
  ): Promise<DriveItem[]> {
    const items: DriveItem[] = [];
    let nextLink: string | undefined = `/drives/${driveId}/root/children`;

    while (nextLink) {
      // 注意: listItem expand 在 Drive API 中不被支持，元数据由本地管理
      const response = await client
        .api(nextLink)
        .top(200)
        .get();

      const pageItems = response?.value ?? [];

      for (const item of pageItems) {
        if (item.file) {
          const itemPath = this.getItemPath(item);
          if (this.shouldIncludeFile(itemPath, includedPaths)) {
            items.push(item);
          }
        } else if (item.folder) {
          const folderPath = this.getItemPath(item);
          if (this.shouldTraverseFolder(folderPath, includedPaths)) {
            // 递归获取文件夹内容
            const folderItems = await this.fetchFolderItems(client, driveId, item.id, includedPaths);
            items.push(...folderItems);
          }
        }
      }

      nextLink = response?.['@odata.nextLink'];
    }

    return items;
  }

  private async fetchDeltaItems(
    client: Client,
    driveId: string,
    scopePath: string,
    options?: { forceStart?: boolean },
  ): Promise<{ items: DeltaDriveItem[]; deltaLink: string | null }> {
    const cursor = await this.prisma.sharePointSyncCursor.findUnique({
      where: {
        driveId_scopePath: {
          driveId,
          scopePath,
        },
      },
    });

    let nextLink: string | undefined;
    if (!options?.forceStart && cursor?.deltaLink) {
      nextLink = cursor.deltaLink;
    } else {
      nextLink = await this.buildDeltaStartUrl(client, driveId, scopePath);
      if (!options?.forceStart) {
        this.logger.warn(
          `SharePoint delta cursor missing for ${scopePath}, starting new delta scan.`,
        );
      }
    }

    const items: DeltaDriveItem[] = [];
    let deltaLink: string | null = null;

    while (nextLink) {
      const request = client.api(nextLink);
      if (!nextLink.includes('$top=')) {
        request.top(200);
      }
      const response = await request.get();
      const pageItems = response?.value ?? [];
      items.push(...pageItems);

      nextLink = response?.['@odata.nextLink'];
      if (!nextLink && response?.['@odata.deltaLink']) {
        deltaLink = response['@odata.deltaLink'];
      }
    }

    if (options?.forceStart) {
      return { items, deltaLink };
    }
    return { items, deltaLink: deltaLink ?? cursor?.deltaLink ?? null };
  }

  private async buildDeltaStartUrl(
    client: Client,
    driveId: string,
    scopePath: string,
  ): Promise<string> {
    if (!scopePath || scopePath === '/') {
      return `/drives/${driveId}/root/delta`;
    }

    const folderId = await this.resolveFolderId(client, driveId, scopePath);
    return `/drives/${driveId}/items/${folderId}/delta`;
  }

  private async resolveFolderId(client: Client, driveId: string, scopePath: string): Promise<string> {
    const encodedPath = this.encodeDrivePath(scopePath);
    const response = await client.api(`/drives/${driveId}/root:${encodedPath}`).get();
    const id = response?.id;
    if (!id) {
      throw new Error(`SharePoint folder not found: ${scopePath}`);
    }
    return id;
  }

  private encodeDrivePath(pathValue: string): string {
    const normalized = this.normalizeSharePointPath(pathValue);
    return normalized
      .split('/')
      .map((segment) => encodeURIComponent(segment))
      .join('/');
  }

  private isRemovedDeltaItem(item: DeltaDriveItem): boolean {
    return Boolean(item.deleted || item.removed || item['@removed']);
  }

  private async fetchFolderItems(
    client: Client,
    driveId: string,
    folderId: string,
    includedPaths: string[],
  ): Promise<DriveItem[]> {
    const items: DriveItem[] = [];
    let nextLink: string | undefined = `/drives/${driveId}/items/${folderId}/children`;

    while (nextLink) {
      // 注意: listItem expand 在 Drive API 中不被支持，元数据由本地管理
      const response = await client
        .api(nextLink)
        .top(200)
        .get();

      const pageItems = response?.value ?? [];

      for (const item of pageItems) {
        if (item.file) {
          const itemPath = this.getItemPath(item);
          if (this.shouldIncludeFile(itemPath, includedPaths)) {
            items.push(item);
          }
        } else if (item.folder) {
          const folderPath = this.getItemPath(item);
          if (this.shouldTraverseFolder(folderPath, includedPaths)) {
            const folderItems = await this.fetchFolderItems(client, driveId, item.id, includedPaths);
            items.push(...folderItems);
          }
        }
      }

      nextLink = response?.['@odata.nextLink'];
    }

    return items;
  }

  private getIncludedPaths(): string[] {
    const raw = this.configService.get<string>('KB_SP_INCLUDED_PATHS') ?? '';
    return raw
      .split(',')
      .map((value) => value.trim())
      .filter(Boolean)
      .map((value) => this.normalizeSharePointPath(value));
  }

  private getItemPath(item: DriveItem): string | null {
    const parentPath = item.parentReference?.path;
    if (!parentPath) {
      return null;
    }
    const rootIndex = parentPath.indexOf('root:');
    if (rootIndex === -1) {
      return null;
    }
    const base = parentPath.slice(rootIndex + 'root:'.length);
    return this.normalizeSharePointPath(`${base}/${item.name}`);
  }

  private normalizeSharePointPath(pathValue: string): string {
    const normalized = pathValue.trim().replace(/\\/g, '/');
    const withLeading = normalized.startsWith('/') ? normalized : `/${normalized}`;
    return withLeading.replace(/\/+$/, '');
  }

  private shouldIncludeFile(pathValue: string | null, includedPaths: string[]): boolean {
    if (includedPaths.length === 0) {
      return true;
    }
    if (!pathValue) {
      return false;
    }
    return includedPaths.some((allowed) => pathValue === allowed || pathValue.startsWith(`${allowed}/`));
  }

  private shouldTraverseFolder(pathValue: string | null, includedPaths: string[]): boolean {
    if (includedPaths.length === 0) {
      return true;
    }
    if (!pathValue) {
      return false;
    }
    return includedPaths.some((allowed) =>
      pathValue === allowed ||
      pathValue.startsWith(`${allowed}/`) ||
      allowed.startsWith(`${pathValue}/`)
    );
  }

  private async cleanupMissingDocuments(currentIds: string[]): Promise<void> {
    if (currentIds.length === 0) {
      await this.prisma.sPDocumentIndex.deleteMany({});
      return;
    }

    await this.prisma.sPDocumentIndex.deleteMany({
      where: { id: { notIn: currentIds } },
    });
  }

  private async cleanupMissingFolders(currentIds: string[]): Promise<void> {
    if (currentIds.length === 0) {
      await this.prisma.sPFolderIndex.deleteMany({});
      return;
    }

    await this.prisma.sPFolderIndex.deleteMany({
      where: { id: { notIn: currentIds } },
    });
  }

  private async cleanupRemovedFolders(removedItemIds: string[], driveId: string): Promise<void> {
    if (removedItemIds.length === 0) {
      return;
    }

    await this.prisma.sPFolderIndex.deleteMany({
      where: {
        spDriveId: driveId,
        spItemId: { in: removedItemIds },
      },
    });
  }

  private parseDocAuthorityLevel(value?: string): DocAuthorityLevel {
    if (!value) return DocAuthorityLevel.DRAFT;

    const normalized = value.toUpperCase().replace(/\s+/g, '_');
    if (normalized in DocAuthorityLevel) {
      return normalized as DocAuthorityLevel;
    }

    return DocAuthorityLevel.DRAFT;
  }

  private parseDocLifecycleStatus(value?: string): DocLifecycleStatus {
    if (!value) return DocLifecycleStatus.DRAFT;

    const normalized = value.toUpperCase().replace(/\s+/g, '_');
    if (normalized in DocLifecycleStatus) {
      return normalized as DocLifecycleStatus;
    }

    return DocLifecycleStatus.DRAFT;
  }

  private parseDocType(value?: string): DocType {
    if (!value) return DocType.GENERAL;

    const normalized = value.toUpperCase().replace(/\s+/g, '_');
    if (normalized in DocType) {
      return normalized as DocType;
    }

    return DocType.GENERAL;
  }

  private getClient(): Client {
    if (this.client) {
      return this.client;
    }

    const tenantId = this.configService.get<string>('AZURE_TENANT_ID');
    const clientId = this.configService.get<string>('AZURE_CLIENT_ID');
    const clientSecret = this.configService.get<string>('AZURE_CLIENT_SECRET');

    if (!tenantId || !clientId || !clientSecret) {
      throw new Error('Missing Azure AD configuration');
    }

    const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
    const authProvider = new TokenCredentialAuthenticationProvider(credential, {
      scopes: ['https://graph.microsoft.com/.default'],
    });

    this.client = Client.initWithMiddleware({ authProvider });
    return this.client;
  }

  private async getSiteId(client: Client): Promise<string> {
    if (this.siteId) {
      return this.siteId;
    }

    const siteUrl = this.configService.get<string>('KB_SP_SITE_URL');
    if (!siteUrl) {
      throw new Error('Missing KB_SP_SITE_URL configuration');
    }

    const url = this.tryParseSiteUrl(siteUrl);
    if (!url) {
      throw new Error('Invalid SharePoint site URL');
    }

    const sitePath = this.normalizeSitePath(url.pathname);
    if (!sitePath) {
      throw new Error('Invalid SharePoint site URL');
    }

    const site = await client.api(`/sites/${url.host}:${sitePath}`).get();

    this.siteId = site?.id;
    if (!this.siteId) {
      throw new Error('Failed to resolve SharePoint site ID');
    }

    return this.siteId;
  }

  private async getDriveId(client: Client): Promise<string> {
    if (this.driveId) {
      return this.driveId;
    }

    const siteId = await this.getSiteId(client);
    const drive = await client.api(`/sites/${siteId}/drive`).get();

    this.driveId = drive?.id;
    if (!this.driveId) {
      throw new Error('Failed to resolve SharePoint drive ID');
    }

    return this.driveId;
  }

  private tryParseSiteUrl(siteUrl: string): URL | null {
    try {
      return new URL(siteUrl);
    } catch {
      try {
        return new URL(encodeURI(siteUrl));
      } catch {
        return null;
      }
    }
  }

  private normalizeSitePath(pathname: string): string | null {
    const segments = pathname.split('/').filter(Boolean);
    if (segments.length < 1) {
      return null;
    }

    const scope = segments[0];
    if (scope !== 'sites' && scope !== 'teams') {
      return `/${segments.join('/')}`;
    }

    if (segments.length < 2) {
      return null;
    }

    return `/${scope}/${segments[1]}`;
  }
}
