import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import type { Model } from "mongoose";
import { Project, type ProjectDocument } from "../schemas/project.schema.js";
import {
  Function as SigmaFunction,
  type FunctionDocument,
} from "../schemas/function.schema.js";

@Injectable()
export class FunctionsService {
  constructor(
    @InjectModel(Project.name)
    private readonly projectModel: Model<Project>,
    @InjectModel(SigmaFunction.name)
    private readonly functionModel: Model<SigmaFunction>
  ) {}

  async findProjectBySlug(slug: string): Promise<ProjectDocument | null> {
    return this.projectModel.findOne({ slug }).exec();
  }

  async findRunnableFunction(
    projectId: ProjectDocument["_id"],
    requestPath: string,
    method: string
  ): Promise<FunctionDocument | null> {
    const functions = await this.functionModel
      .find({ project: projectId })
      .exec();

    const normalizedPath = this.normalizePath(requestPath);
    const upperMethod = method.toUpperCase();

    for (const func of functions) {
      if (!this.supportsMethod(func, upperMethod)) {
        continue;
      }

      if (this.pathMatches(func.path, normalizedPath)) {
        return func;
      }
    }

    return null;
  }

  async ensureCpuQuota(project: ProjectDocument): Promise<void> {
    const quota = project.cpuTimeQuotaMsPerMinute ?? 1000;
    const updated = this.resetUsageWindowIfNeeded(project);
    if (project.cpuTimeUsedMs >= quota) {
      throw new HttpException(
        `CPU time quota exceeded for project ${project.slug}`,
        HttpStatus.TOO_MANY_REQUESTS
      );
    }

    if (updated) {
      await project.save();
    }
  }

  async recordCpuUsage(
    project: ProjectDocument,
    elapsedMs: number
  ): Promise<void> {
    const quota = project.cpuTimeQuotaMsPerMinute ?? 1000;
    this.resetUsageWindowIfNeeded(project);
    project.cpuTimeUsedMs = Math.min(
      quota,
      (project.cpuTimeUsedMs ?? 0) + Math.max(0, Math.round(elapsedMs))
    );

    await project.save();
  }

  async recordInvocation(
    functionId: string,
    successful: boolean,
    logs: string[]
  ): Promise<void> {
    const func = await this.functionModel.findByIdAndUpdate(functionId, {
      $inc: { invocationCount: 1, errorCount: successful ? 0 : 1 },
      lastInvocation: new Date(),
      $push: { logs: { $each: logs, $slice: -100 } },
    });

    if (func) {
      await this.projectModel.findByIdAndUpdate(func.project, {
        $inc: {
          functionsInvocationCount: 1,
          functionsErrorCount: successful ? 0 : 1,
        },
      });
    }
  }

  private supportsMethod(func: FunctionDocument, method: string): boolean {
    if (!func.methods || func.methods.length === 0) {
      return true;
    }

    return func.methods.some((allowed) => allowed.toUpperCase() === method);
  }

  private pathMatches(pattern: string, requestPath: string): boolean {
    const normalizedPattern = this.normalizePath(pattern);
    if (normalizedPattern === "/*") {
      return true;
    }

    const patternSegments = normalizedPattern.split("/").filter(Boolean);
    const requestSegments = requestPath.split("/").filter(Boolean);

    if (patternSegments.length !== requestSegments.length) {
      return false;
    }

    return patternSegments.every((segment, index) => {
      const value = requestSegments[index];
      if (segment.startsWith(":")) {
        return typeof value === "string" && value.length > 0;
      }
      return segment === value;
    });
  }

  private normalizePath(input: string): string {
    if (!input || input === "") {
      return "/";
    }

    let normalized = input.startsWith("/") ? input : `/${input}`;
    if (normalized.length > 1 && normalized.endsWith("/")) {
      normalized = normalized.slice(0, -1);
    }
    return normalized;
  }

  private resetUsageWindowIfNeeded(project: ProjectDocument): boolean {
    const now = Date.now();
    const startedAt = project.cpuTimeWindowStartedAt?.getTime() ?? 0;
    if (now - startedAt >= 60_000) {
      project.cpuTimeWindowStartedAt = new Date(now);
      project.cpuTimeUsedMs = 0;
      return true;
    }

    if (!project.cpuTimeWindowStartedAt) {
      project.cpuTimeWindowStartedAt = new Date(now);
      project.cpuTimeUsedMs = 0;
      return true;
    }

    if (project.cpuTimeUsedMs == null) {
      project.cpuTimeUsedMs = 0;
      return true;
    }

    return false;
  }
}
