import * as path from 'path';
import * as bcrypt from 'bcrypt';
import sanitize = require('sanitize-filename');
import { FastifyRequest } from 'fastify';
import { Readable, Transform } from 'stream';

export const envPath = path.resolve(process.cwd(), '..', '..', '.env');

export async function hashPassword(password: string) {
  const saltRounds = 12;
  return bcrypt.hash(password, saltRounds);
}

export async function comparePasswordHash(
  plainPassword: string,
  passwordHash: string,
): Promise<boolean> {
  return bcrypt.compare(plainPassword, passwordHash);
}

export function generateRandomSuffixNumbers(length: number) {
  return Math.random()
    .toFixed(length)
    .substring(2, 2 + length);
}

export type RedisConfig = {
  host: string;
  port: number;
  db: number;
  password?: string;
  family?: number;
};

export function parseRedisUrl(redisUrl: string): RedisConfig {
  // format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6]
  const url = new URL(redisUrl);
  const { hostname, port, password, pathname, searchParams } = url;
  const portInt = parseInt(port, 10);

  let db: number = 0;
  // extract db value if present
  if (pathname.length > 1) {
    const value = pathname.slice(1);
    if (!isNaN(parseInt(value))) {
      db = parseInt(value, 10);
    }
  }

  // extract family from query parameters
  let family: number | undefined;
  const familyParam = searchParams.get('family');
  if (familyParam && !isNaN(parseInt(familyParam))) {
    family = parseInt(familyParam, 10);
  }

  return { host: hostname, port: portInt, password, db, family };
}

export function createRetryStrategy() {
  return function (times: number): number {
    return Math.max(Math.min(Math.exp(times), 20000), 3000);
  };
}

export function extractDateFromUuid7(uuid7: string) {
  //https://park.is/blog_posts/20240803_extracting_timestamp_from_uuid_v7/
  const parts = uuid7.split('-');
  const highBitsHex = parts[0] + parts[1].slice(0, 4);
  const timestamp = parseInt(highBitsHex, 16);

  return new Date(timestamp);
}

export type SanitizeFileNameOptions = {
  /** Keep spaces and `#` instead of replacing them with `_`. Useful for
   * download filenames where readability matters. Defaults to false. */
  preserveSpaces?: boolean;
};

export function sanitizeFileName(
  fileName: string,
  options: SanitizeFileNameOptions = {},
): string {
  // Decode percent-encoded sequences so that bypasses like "..%2F" reach
  // sanitize() as literal "../" and get stripped. sanitize-filename only
  // strips literal characters and won't catch encoded path separators
  // on its own.
  const decoded = fileName.replace(/%[0-9a-fA-F]{2}/g, (m) => {
    try {
      return decodeURIComponent(m);
    } catch {
      return m;
    }
  });

  const sanitized = sanitize(decoded);
  if (options.preserveSpaces) {
    return sanitized;
  }
  return sanitized.replace(/ /g, '_').replace(/#/g, '_');
}

export function removeAccent(str: string): string {
  if (!str) return str;
  return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}

export function extractBearerTokenFromHeader(
  request: FastifyRequest,
): string | undefined {
  const [type, token] = request.headers.authorization?.split(' ') ?? [];
  return type?.toLowerCase() === 'bearer' ? token : undefined;
}

/**
 * Normalizes a database URL for postgres.js compatibility.
 * - Removes `sslmode=no-verify` (not supported by postgres.js), keeps other sslmode values
 * - Removes `schema` parameter (has no effect via connection string)
 * Note: If we don't strip them, the connection will fail
 */
export function normalizePostgresUrl(url: string): string {
  const parsed = new URL(url);
  const newParams = new URLSearchParams();

  for (const [key, value] of parsed.searchParams) {
    if (key === 'sslmode' && value === 'no-verify') continue;
    if (key === 'schema') continue;
    newParams.append(key, value);
  }

  parsed.search = newParams.toString();
  return parsed.toString();
}

export function diffAuditTrackedFields(
  fields: readonly string[],
  dto: Record<string, any>,
  before: Record<string, any> | undefined | null,
  after: Record<string, any> | undefined | null,
): { before: Record<string, any>; after: Record<string, any> } | null {
  const beforeDiff: Record<string, any> = {};
  const afterDiff: Record<string, any> = {};
  let hasChanges = false;

  for (const field of fields) {
    if (typeof dto[field] === 'undefined') continue;
    const oldVal = JSON.stringify(before?.[field] ?? null);
    const newVal = JSON.stringify(after?.[field] ?? null);
    if (oldVal !== newVal) {
      beforeDiff[field] = before?.[field];
      afterDiff[field] = after?.[field];
      hasChanges = true;
    }
  }

  return hasChanges ? { before: beforeDiff, after: afterDiff } : null;
}

export function isUserDisabled(user: {
  deactivatedAt?: Date | null;
  deletedAt?: Date | null;
}): boolean {
  return !!(user.deactivatedAt || user.deletedAt);
}

const SENSITIVE_URL_PREFIXES = ['/api/sso/'];

export function redactSensitiveUrl(url: string): string {
  if (url && SENSITIVE_URL_PREFIXES.some((prefix) => url.includes(prefix))) {
    const qsIndex = url.indexOf('?');
    if (qsIndex !== -1) {
      return url.substring(0, qsIndex);
    }
  }
  return url;
}

export function createByteCountingStream(source: Readable) {
  let bytesRead = 0;
  const stream = new Transform({
    transform(chunk, encoding, callback) {
      bytesRead += chunk.length;
      callback(null, chunk);
    },
  });

  source.pipe(stream);
  source.on('error', (err) => stream.emit('error', err));

  return { stream, getBytesRead: () => bytesRead };
}
