import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ClientSecretCredential } from '@azure/identity';
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { parse as parseCsv } from 'csv-parse/sync';
import {
  AzureCredentialMissingException,
  GraphAuthFailedException,
  GraphInsufficientScopeException,
  GraphRateLimitException,
  GraphUpstreamErrorException,
  ReportsObfuscatedException,
} from '../m365-dormant.exceptions';
import {
  ActivityReportEntry,
  ActivityReportName,
  GraphClient,
  GraphSubscribedSku,
  GraphUser,
} from './graph-client.interface';

const USER_SELECT_FIELDS = [
  'id',
  'userPrincipalName',
  'displayName',
  'mail',
  'department',
  'jobTitle',
  'accountEnabled',
  'createdDateTime',
  'signInActivity',
  'assignedLicenses',
].join(',');

const REPORT_ENDPOINTS: Record<ActivityReportName, string> = {
  EmailActivity: "/reports/getEmailActivityUserDetail(period='D180')",
  OneDriveActivity: "/reports/getOneDriveActivityUserDetail(period='D180')",
  TeamsUserActivity: "/reports/getTeamsUserActivityUserDetail(period='D180')",
  SharePointActivity: "/reports/getSharePointActivityUserDetail(period='D180')",
};

@Injectable()
export class RealGraphClient implements GraphClient {
  private readonly logger = new Logger(RealGraphClient.name);
  private client: Client | null = null;

  constructor(private readonly configService: ConfigService) {}

  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 missing: string[] = [];
    if (!tenantId) missing.push('AZURE_TENANT_ID');
    if (!clientId) missing.push('AZURE_CLIENT_ID');
    if (!clientSecret) missing.push('AZURE_CLIENT_SECRET');
    if (missing.length > 0) throw new AzureCredentialMissingException(missing);

    try {
      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;
    } catch (error: any) {
      this.logger.error('Graph client init failed', error?.message);
      throw new GraphAuthFailedException(error?.message);
    }
  }

  async listSubscribedSkus(): Promise<GraphSubscribedSku[]> {
    const client = this.getClient();
    try {
      const res = await client.api('/subscribedSkus').get();
      return (res.value ?? []).map((s: any) => ({
        skuId: s.skuId,
        skuPartNumber: s.skuPartNumber,
      }));
    } catch (error: any) {
      throw this.translateGraphError(error);
    }
  }

  async listUsers(): Promise<GraphUser[]> {
    const client = this.getClient();
    const all: GraphUser[] = [];

    try {
      let request = client.api('/users').select(USER_SELECT_FIELDS).top(999);

      // 处理 ConsistencyLevel header for $select with signInActivity
      request = request.header('ConsistencyLevel', 'eventual');

      let page: any = await request.get();
      while (page) {
        for (const u of page.value ?? []) {
          all.push({
            id: u.id,
            userPrincipalName: u.userPrincipalName,
            displayName: u.displayName ?? null,
            mail: u.mail ?? null,
            department: u.department ?? null,
            jobTitle: u.jobTitle ?? null,
            accountEnabled: !!u.accountEnabled,
            createdDateTime: u.createdDateTime ?? null,
            signInActivity: u.signInActivity ?? null,
            assignedLicenses: u.assignedLicenses ?? [],
          });
        }
        if (page['@odata.nextLink']) {
          page = await client.api(page['@odata.nextLink']).get();
        } else {
          page = null;
        }
      }

      return all;
    } catch (error: any) {
      throw this.translateGraphError(error);
    }
  }

  async getActivityReport(report: ActivityReportName): Promise<ActivityReportEntry[]> {
    const client = this.getClient();
    const path = REPORT_ENDPOINTS[report];

    // Reports API returns text/csv with BOM via redirected stream
    let csvText: string;
    try {
      const response = await client.api(path).getStream();
      csvText = await streamToString(response);
    } catch (error: any) {
      throw this.translateGraphError(error);
    }

    const rows = parseCsv(csvText, {
      columns: true,
      skip_empty_lines: true,
      bom: true,
    }) as Array<Record<string, string>>;

    if (rows.length > 0) {
      const firstUpn = rows[0]['User Principal Name'] ?? '';
      if (!firstUpn.includes('@')) {
        // Obfuscation detected — UPN is hashed
        throw new ReportsObfuscatedException();
      }
    }

    return rows.map((row: any) => ({
      userPrincipalNameLower: String(row['User Principal Name'] ?? '').toLowerCase(),
      lastActivityDate: parseReportDate(row['Last Activity Date']),
    }));
  }

  /** Translate Graph SDK errors into structured project exceptions. */
  private translateGraphError(error: any): never {
    const status = error?.statusCode ?? error?.status;
    const code = error?.code ?? error?.body?.error?.code;
    const message = error?.message ?? error?.body?.error?.message ?? String(error);

    if (status === 401 || code === 'InvalidAuthenticationToken') {
      throw new GraphAuthFailedException(message);
    }
    if (status === 403 || code === 'Authorization_RequestDenied') {
      throw new GraphInsufficientScopeException(message);
    }
    if (status === 429) {
      throw new GraphRateLimitException(message);
    }
    throw new GraphUpstreamErrorException(message);
  }
}

async function streamToString(stream: any): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of stream) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  return Buffer.concat(chunks).toString('utf-8');
}

function parseReportDate(raw: string | undefined | null): Date | null {
  if (!raw || raw === '') return null;
  const d = new Date(raw);
  return Number.isNaN(d.getTime()) ? null : d;
}
