import { createHash, randomUUID } from 'node:crypto';

import {
  Injectable,
  NotFoundException,
} from '@nestjs/common';

import type {
  WhmcsUsageAdjustmentRequest,
  WhmcsUsageResponse,
} from '@hexahost/contracts';
import type { Prisma } from '@hexahost/database';

import { PrismaService } from '../../prisma/prisma.service';

import { WhmcsEventsService } from './whmcs-events.service';
import { resolveBillingPeriod } from './whmcs-period.util';

type InstallationRef = {
  id: string;
};

export type WhmcsUsageMetrics = {
  runtime_minutes: number;
  memory_gib_minutes: number;
  cpu_core_minutes: number;
  storage_gib_days: number;
  backup_storage_gib_days: number;
  egress_gib: number;
  extra_port_days: number;
  priority_queue_minutes: number;
  managed_service_units: number;
};

@Injectable()
export class WhmcsUsageService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly events: WhmcsEventsService,
  ) {}

  async getServiceUsage(
    installation: InstallationRef,
    externalServiceId: string,
    periodStart?: string,
    periodEnd?: string,
    testMode = false,
  ): Promise<WhmcsUsageResponse> {
    const link = await this.prisma.whmcsServiceLink.findUnique({
      where: {
        installationId_externalServiceId: {
          installationId: installation.id,
          externalServiceId,
        },
      },
      include: {
        server: { include: { plan: true } },
      },
    });

    if (!link) {
      throw new NotFoundException(`Unknown WHMCS service ${externalServiceId}`);
    }

    const period = resolveBillingPeriod(periodStart, periodEnd);

    const existing = await this.prisma.whmcsUsageExport.findUnique({
      where: {
        installationId_externalServiceId_periodStart_periodEnd_isTestMode: {
          installationId: installation.id,
          externalServiceId,
          periodStart: period.start,
          periodEnd: period.end,
          isTestMode: testMode,
        },
      },
    });

    if (existing) {
      return this.toUsageResponse(existing, externalServiceId);
    }

    const metrics = await this.aggregateMetrics(
      link.serverId,
      period.start,
      period.end,
      link.server.plan?.maxCpuCores ?? 1,
      link.server.plan?.maxStorageMb ?? 5120,
      link.server.plan?.queuePriority ?? 0,
    );

    const contentHash = this.hashMetrics(metrics);
    const usageUpdate = this.toUsageUpdate(metrics);

    const created = await this.prisma.whmcsUsageExport.create({
      data: {
        installationId: installation.id,
        externalServiceId,
        serverId: link.serverId,
        periodStart: period.start,
        periodEnd: period.end,
        metrics: metrics as Prisma.InputJsonValue,
        contentHash,
        status: testMode ? 'DRAFT' : 'EXPORTED',
        isTestMode: testMode,
        exportedAt: testMode ? null : new Date(),
      },
    });

    if (!testMode) {
      await this.events.emitEvent(
        installation.id,
        'usage_period_closed',
        {
          exportId: created.id,
          externalServiceId,
          periodStart: period.start.toISOString(),
          periodEnd: period.end.toISOString(),
          contentHash,
        },
        `usage:${installation.id}:${externalServiceId}:${period.start.toISOString()}:${period.end.toISOString()}`,
      );
    }

    return this.toUsageResponse(created, externalServiceId, usageUpdate);
  }

  async createAdjustment(
    installation: InstallationRef,
    externalServiceId: string,
    input: WhmcsUsageAdjustmentRequest,
    idempotencyKey: string,
  ): Promise<WhmcsUsageResponse> {
    const existingOp = await this.prisma.integrationOperation.findUnique({
      where: { idempotencyKey },
    });
    if (existingOp?.result) {
      return existingOp.result as WhmcsUsageResponse;
    }

    const link = await this.prisma.whmcsServiceLink.findUnique({
      where: {
        installationId_externalServiceId: {
          installationId: installation.id,
          externalServiceId,
        },
      },
    });

    if (!link) {
      throw new NotFoundException(`Unknown WHMCS service ${externalServiceId}`);
    }

    const periodStart = new Date(input.periodStart);
    const periodEnd = new Date(input.periodEnd);

    const baseExport = await this.prisma.whmcsUsageExport.findUnique({
      where: {
        installationId_externalServiceId_periodStart_periodEnd_isTestMode: {
          installationId: installation.id,
          externalServiceId,
          periodStart,
          periodEnd,
          isTestMode: input.testMode,
        },
      },
    });

    if (!baseExport) {
      throw new NotFoundException('No usage export exists for this period');
    }

    const baseMetrics = baseExport.metrics as WhmcsUsageMetrics;
    const adjustedMetrics = { ...baseMetrics };

    for (const adjustment of input.adjustments) {
      const key = adjustment.metricKey as keyof WhmcsUsageMetrics;
      if (key in adjustedMetrics) {
        adjustedMetrics[key] = roundMetric(adjustedMetrics[key] + adjustment.delta);
      }
    }

    const contentHash = this.hashMetrics(adjustedMetrics);
    const usageUpdate = this.toUsageUpdate(adjustedMetrics);

    const adjustmentExport = await this.prisma.whmcsUsageExport.update({
      where: { id: baseExport.id },
      data: {
        metrics: adjustedMetrics as Prisma.InputJsonValue,
        contentHash,
        status: 'ADJUSTED',
        adjustmentOfId: baseExport.adjustmentOfId ?? baseExport.id,
        exportedAt: new Date(),
      },
    });

    const response = this.toUsageResponse(
      adjustmentExport,
      externalServiceId,
      usageUpdate,
      input.adjustments,
    );

    await this.prisma.integrationOperation.create({
      data: {
        installationId: installation.id,
        idempotencyKey,
        operation: 'usage:adjustment',
        externalRef: externalServiceId,
        status: 'COMPLETED',
        result: response as object,
      },
    });

    return response;
  }

  private async aggregateMetrics(
    serverId: string,
    periodStart: Date,
    periodEnd: Date,
    cpuCores: number,
    storageMb: number,
    queuePriority: number,
  ): Promise<WhmcsUsageMetrics> {
    const usageRecords = await this.prisma.usageRecord.findMany({
      where: {
        serverId,
        periodStart: { gte: periodStart },
        periodEnd: { lte: periodEnd },
      },
    });

    let runtimeSeconds = 0;
    let memoryMbSeconds = 0;

    for (const record of usageRecords) {
      runtimeSeconds += record.durationSeconds;
      memoryMbSeconds += record.ramMb * record.durationSeconds;
    }

    const periodDays = Math.max(
      (periodEnd.getTime() - periodStart.getTime()) / (24 * 60 * 60 * 1000),
      1,
    );

    const backups = await this.prisma.serverBackup.findMany({
      where: {
        serverId,
        status: 'AVAILABLE',
        completedAt: {
          gte: periodStart,
          lt: periodEnd,
        },
      },
      select: { sizeBytes: true },
    });

    let backupStorageGiBDays = 0;
    for (const backup of backups) {
      const sizeGiB = Number(backup.sizeBytes ?? 0n) / (1024 ** 3);
      backupStorageGiBDays += sizeGiB * periodDays;
    }

    const runtimeMinutes = roundMetric(runtimeSeconds / 60);
    const memoryGibMinutes = roundMetric(memoryMbSeconds / 1024 / 60);
    const cpuCoreMinutes = roundMetric((runtimeSeconds / 60) * cpuCores);
    const storageGibDays = roundMetric((storageMb / 1024) * periodDays);
    const priorityQueueMinutes =
      queuePriority > 0 ? roundMetric(runtimeMinutes * (queuePriority / 10)) : 0;

    return {
      runtime_minutes: runtimeMinutes,
      memory_gib_minutes: memoryGibMinutes,
      cpu_core_minutes: cpuCoreMinutes,
      storage_gib_days: storageGibDays,
      backup_storage_gib_days: roundMetric(backupStorageGiBDays),
      egress_gib: 0,
      extra_port_days: 0,
      priority_queue_minutes: priorityQueueMinutes,
      managed_service_units: 0,
    };
  }

  private hashMetrics(metrics: WhmcsUsageMetrics): string {
    return createHash('sha256').update(JSON.stringify(metrics)).digest('hex');
  }

  private toUsageUpdate(metrics: WhmcsUsageMetrics) {
    return {
      diskUsageGiB: metrics.storage_gib_days,
      bandwidthGiB: metrics.egress_gib,
    };
  }

  private toUsageResponse(
    exportRow: {
      id: string;
      serverId: string;
      periodStart: Date;
      periodEnd: Date;
      metrics: unknown;
      contentHash: string;
      status: 'DRAFT' | 'EXPORTED' | 'ADJUSTED';
      isTestMode: boolean;
    },
    externalServiceId: string,
    usageUpdate?: { diskUsageGiB: number; bandwidthGiB: number },
    adjustments: WhmcsUsageResponse['adjustments'] = [],
  ): WhmcsUsageResponse {
    const metrics = exportRow.metrics as WhmcsUsageMetrics;
    return {
      exportId: exportRow.id,
      externalServiceId,
      serverId: exportRow.serverId,
      periodStart: exportRow.periodStart.toISOString(),
      periodEnd: exportRow.periodEnd.toISOString(),
      metrics,
      usageUpdate: usageUpdate ?? this.toUsageUpdate(metrics),
      contentHash: exportRow.contentHash,
      status: exportRow.status,
      isTestMode: exportRow.isTestMode,
      adjustments,
    };
  }
}

function roundMetric(value: number): number {
  return Math.round(value * 1000) / 1000;
}
