import { BadRequestException, Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Client } from '@microsoft/microsoft-graph-client';
import { ClientSecretCredential } from '@azure/identity';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import * as fs from 'fs';

export interface SharePointUploadResult {
  id: string;
  name: string;
  webUrl: string;
  size: number | null;
  createdAt: string | null;
  lastModifiedAt: string | null;
}

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

  constructor(private readonly configService: ConfigService) {}

  async uploadFile(filePath: string, originalName: string): Promise<SharePointUploadResult> {
    const client = this.getClient();
    const driveId = await this.getDriveId(client);
    const stat = await fs.promises.stat(filePath);
    const safeName = encodeURI(originalName);

    try {
      const uploadSession = await client
        .api(`/drives/${driveId}/root:/${safeName}:/createUploadSession`)
        .post({
          item: {
            '@microsoft.graph.conflictBehavior': 'rename',
          },
        });

      const uploadUrl = uploadSession?.uploadUrl;
      if (!uploadUrl) {
        throw new Error('upload_url_not_found');
      }

      const item = await this.uploadByChunks(uploadUrl, filePath, stat.size, client, driveId, safeName);
      return {
        id: item.id,
        name: item.name,
        webUrl: item.webUrl,
        size: item.size ?? null,
        createdAt: item.createdDateTime ?? null,
        lastModifiedAt: item.lastModifiedDateTime ?? null,
      };
    } catch (error: any) {
      const details = this.formatGraphError(error);
      const rawBody = error?.body ? JSON.stringify(error.body) : undefined;
      this.logger.error('SharePoint upload failed', details);
      if (rawBody) {
        this.logger.error(`SharePoint error body: ${rawBody}`);
      }
      throw new ServiceUnavailableException({
        code: 'KNOWLEDGE_BASE_UPLOAD_FAILED',
        message: 'SharePoint 上传失败，请稍后重试',
        details,
      });
    }
  }

  private async uploadByChunks(
    uploadUrl: string,
    filePath: string,
    totalSize: number,
    client: Client,
    driveId: string,
    safeName: string,
  ) {
    const chunkSize = 320 * 1024 * 10; // 3.2MB, must be multiple of 320KB
    const fileHandle = await fs.promises.open(filePath, 'r');
    let offset = 0;
    let lastResponse: any = null;

    try {
      while (offset < totalSize) {
        const length = Math.min(chunkSize, totalSize - offset);
        const buffer = Buffer.alloc(length);
        const { bytesRead } = await fileHandle.read(buffer, 0, length, offset);
        const chunk = buffer.subarray(0, bytesRead);

        const rangeStart = offset;
        const rangeEnd = offset + bytesRead - 1;
        const contentRange = `bytes ${rangeStart}-${rangeEnd}/${totalSize}`;

        const response = await fetch(uploadUrl, {
          method: 'PUT',
          headers: {
            'Content-Length': bytesRead.toString(),
            'Content-Range': contentRange,
          },
          body: chunk,
        });

        const text = await response.text();
        const data = text ? JSON.parse(text) : {};

        if (!response.ok) {
          throw Object.assign(new Error('upload_chunk_failed'), {
            statusCode: response.status,
            body: data,
          });
        }

        lastResponse = data;
        offset += bytesRead;

        if (data?.id) {
          break;
        }
      }

      if (!lastResponse?.id) {
        const fallback = await this.fetchDriveItemByPath(client, driveId, safeName);
        if (!fallback) {
          throw new Error('upload_incomplete');
        }
        return fallback;
      }

      return lastResponse;
    } finally {
      await fileHandle.close();
    }
  }

  private async fetchDriveItemByPath(client: Client, driveId: string, safeName: string) {
    try {
      return await client.api(`/drives/${driveId}/root:/${safeName}`).get();
    } catch {
      return null;
    }
  }

  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');

    const missingConfig: string[] = [];
    if (!tenantId) missingConfig.push('AZURE_TENANT_ID');
    if (!clientId) missingConfig.push('AZURE_CLIENT_ID');
    if (!clientSecret) missingConfig.push('AZURE_CLIENT_SECRET');

    if (missingConfig.length > 0) {
      throw new BadRequestException({
        code: 'KNOWLEDGE_BASE_CONFIG_MISSING',
        message: `缺少 Azure AD 配置: ${missingConfig.join(', ')}`,
      });
    }

    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');
    const resolved = this.resolveSiteConfig(siteUrl);
    if (!resolved) {
      throw new BadRequestException({
        code: 'KNOWLEDGE_BASE_CONFIG_MISSING',
        message: '缺少 SharePoint 站点配置（KB_SP_SITE_URL）',
      });
    }

    try {
      const site = await client.api(`/sites/${resolved.host}:${resolved.path}`).get();
      this.siteId = site?.id;
      if (!this.siteId) {
        throw new Error('site_id_not_found');
      }
      return this.siteId;
    } catch (error: any) {
      const details = this.formatGraphError(error);
      this.logger.error('Resolve SharePoint site failed', details);
      throw new ServiceUnavailableException({
        code: 'KNOWLEDGE_BASE_SITE_RESOLVE_FAILED',
        message: '无法解析 SharePoint 站点，请检查配置',
        details,
      });
    }
  }

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

    const siteId = await this.getSiteId(client);
    try {
      const drive = await client.api(`/sites/${siteId}/drive`).get();
      this.driveId = drive?.id;
      if (!this.driveId) {
        throw new Error('drive_id_not_found');
      }
      return this.driveId;
    } catch (error: any) {
      const details = this.formatGraphError(error);
      this.logger.error('Resolve SharePoint drive failed', details);
      throw new ServiceUnavailableException({
        code: 'KNOWLEDGE_BASE_DRIVE_RESOLVE_FAILED',
        message: '无法解析 SharePoint 文档库，请检查权限与站点配置',
        details,
      });
    }
  }

  private resolveSiteConfig(
    siteUrl?: string,
  ): { host: string; path: string } | null {
    if (siteUrl) {
      const url = this.tryParseSiteUrl(siteUrl);
      if (url) {
        const trimmedPath = this.normalizeSitePath(url.pathname);
        if (trimmedPath) {
          return { host: url.host, path: trimmedPath };
        }
      }
    }
    return null;
  }

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

  private formatGraphError(error: any): string {
    if (!error) {
      return 'unknown_error';
    }

    const statusCode = error.statusCode || error.status;
    const code = error.code || error.body?.error?.code;
    const message = error.message || error.body?.error?.message;
    const requestId = error.body?.error?.innerError?.['request-id'];

    return JSON.stringify({
      statusCode,
      code,
      message,
      requestId,
    });
  }
}
