import { Injectable, BadRequestException } from "@nestjs/common";
import crypto from "crypto";

type PasswordlessSettingsTemplate = {
  token_ttl_seconds: number;
  token_length: number;
  resend_cooldown_seconds: number;
  max_attempts: number;
  delivery_methods: string[];
  magic_link_base_url: string;
};

type PasswordlessSettings = {
  tokenTtlSeconds: number;
  tokenLength: number;
  resendCooldownSeconds: number;
  maxAttempts: number;
  deliveryMethods: string[];
  magicLinkBaseUrl: string;
};

type PasswordlessToken = {
  tokenId: string;
  identifier: string;
  deliveryMethod: string;
  code: string;
  issuedAt: number;
  expiresAt: number;
  attempts: number;
  metadata: Record<string, unknown>;
};

type MagicLinkResult = {
  tokenId: string;
  url: string;
  expiresAt: number;
};

type PasswordlessMetadata = {
  module: string;
  tokenTtlSeconds: number;
  tokenLength: number;
  resendCooldownSeconds: number;
  maxAttempts: number;
  deliveryMethods: string[];
  magicLinkBaseUrl: string;
  supportsMagicLinks: boolean;
  supportsCodes: boolean;
  features: string[];
};

const RAW_SETTINGS: PasswordlessSettingsTemplate = {{ module_defaults | default({}) | tojson(indent=2) }};

const FEATURE_FLAGS = [
  "magic_links",
  "one_time_codes",
  "rate_limiting",
  "delivery_method_controls",
] as const;

const chooseFrom = (alphabet: string, length: number): string => {
  const bytes = crypto.randomBytes(length);
  let output = "";
  for (let i = 0; i < length; i += 1) {
    output += alphabet[bytes[i] % alphabet.length];
  }
  return output;
};

const normalizeSettings = (template: PasswordlessSettingsTemplate): PasswordlessSettings => ({
  tokenTtlSeconds: template.token_ttl_seconds,
  tokenLength: template.token_length,
  resendCooldownSeconds: template.resend_cooldown_seconds,
  maxAttempts: template.max_attempts,
  deliveryMethods: template.delivery_methods.map((method) => method.toLowerCase()),
  magicLinkBaseUrl: template.magic_link_base_url,
});

const buildMagicLink = (settings: PasswordlessSettings, token: PasswordlessToken): string => {
  const base = settings.magicLinkBaseUrl.replace(/\/$/, "");
  const params = new URLSearchParams({
    token: token.tokenId,
    code: token.code,
    identifier: token.identifier,
  });
  return `${base}?${params.toString()}`;
};

const describeRuntime = (settings: PasswordlessSettings): PasswordlessMetadata => ({
  module: "passwordless",
  tokenTtlSeconds: settings.tokenTtlSeconds,
  tokenLength: settings.tokenLength,
  resendCooldownSeconds: settings.resendCooldownSeconds,
  maxAttempts: settings.maxAttempts,
  deliveryMethods: [...settings.deliveryMethods],
  magicLinkBaseUrl: settings.magicLinkBaseUrl,
  supportsMagicLinks: true,
  supportsCodes: true,
  features: FEATURE_FLAGS.map((flag) => flag as string),
});

@Injectable()
export class {{ module_class_name }}Service {
  private readonly settings = normalizeSettings(RAW_SETTINGS);
  private readonly tokens = new Map<string, PasswordlessToken>();
  private readonly byRecipient = new Map<string, string>();
  private readonly lastIssued = new Map<string, number>();

  issueCode(
    identifier: string,
    deliveryMethod = "email",
    metadata: Record<string, unknown> = {},
  ): PasswordlessToken {
    const method = deliveryMethod.toLowerCase();
    if (!this.settings.deliveryMethods.includes(method)) {
      throw new BadRequestException(`Unsupported delivery method: ${deliveryMethod}`);
    }

    const recipientKey = this.recipientKey(identifier, method);
    const now = Date.now() / 1000;
    const lastSent = this.lastIssued.get(recipientKey) ?? 0;
    if (now - lastSent < this.settings.resendCooldownSeconds) {
      throw new BadRequestException("Passwordless token recently sent; try again later");
    }

    const token: PasswordlessToken = {
      tokenId: crypto.randomUUID(),
      identifier,
      deliveryMethod: method,
      code: chooseFrom("0123456789", this.settings.tokenLength),
      issuedAt: now,
      expiresAt: now + this.settings.tokenTtlSeconds,
      attempts: 0,
      metadata,
    };

    this.tokens.set(token.tokenId, token);
    this.byRecipient.set(recipientKey, token.tokenId);
    this.lastIssued.set(recipientKey, now);
    return token;
  }

  issueMagicLink(
    identifier: string,
    deliveryMethod = "email",
    metadata: Record<string, unknown> = {},
  ): MagicLinkResult {
    const token = this.issueCode(identifier, deliveryMethod, metadata);
    return {
      tokenId: token.tokenId,
      url: buildMagicLink(this.settings, token),
      expiresAt: token.expiresAt,
    };
  }

  verifyCode(identifier: string, code: string, deliveryMethod?: string): PasswordlessToken {
    const method = deliveryMethod?.toLowerCase();
    if (method && !this.settings.deliveryMethods.includes(method)) {
      throw new BadRequestException(`Unsupported delivery method: ${deliveryMethod}`);
    }

    const recipientKey = method
      ? this.recipientKey(identifier, method)
      : this.resolveRecipientKey(identifier);

    const tokenId = this.byRecipient.get(recipientKey);
    if (!tokenId) {
      throw new BadRequestException("No active passwordless token for identifier");
    }

    const token = this.tokens.get(tokenId);
    const now = Date.now() / 1000;
    if (!token || token.expiresAt <= now) {
      this.invalidate(tokenId);
      throw new BadRequestException("Passwordless token has expired");
    }

    if (token.attempts >= this.settings.maxAttempts) {
      this.invalidate(tokenId);
      throw new BadRequestException("Maximum verification attempts exceeded");
    }

    token.attempts += 1;
    const expected = Buffer.from(token.code);
    const provided = Buffer.from(code);
    if (expected.length !== provided.length || !crypto.timingSafeEqual(expected, provided)) {
      throw new BadRequestException("Passwordless code is invalid");
    }

    this.invalidate(tokenId);
    return token;
  }

  purgeExpired(): void {
    const now = Date.now() / 1000;
    for (const [tokenId, token] of this.tokens.entries()) {
      if (token.expiresAt <= now) {
        this.invalidate(tokenId);
      }
    }
  }

  describe(): PasswordlessMetadata {
    return describeRuntime(this.settings);
  }

  listFeatures(): string[] {
    return FEATURE_FLAGS.map((flag) => flag as string);
  }

  listDeliveryMethods(): string[] {
    return [...this.settings.deliveryMethods];
  }

  private invalidate(tokenId: string): void {
    const token = this.tokens.get(tokenId);
    if (!token) {
      return;
    }
    this.tokens.delete(tokenId);
    const key = this.recipientKey(token.identifier, token.deliveryMethod);
    if (this.byRecipient.get(key) === tokenId) {
      this.byRecipient.delete(key);
    }
  }

  private resolveRecipientKey(identifier: string): string {
    for (const method of this.settings.deliveryMethods) {
      const key = this.recipientKey(identifier, method);
      if (this.byRecipient.has(key)) {
        return key;
      }
    }
    throw new BadRequestException("No active passwordless token for identifier");
  }

  private recipientKey(identifier: string, deliveryMethod: string): string {
    return `${identifier.toLowerCase()}::${deliveryMethod.toLowerCase()}`;
  }
}
