import crypto from 'crypto';

import type { S3ImageStorageConfig } from './imageStorageConfig.js';

const awsDate = (date: Date) => date.toISOString().replace(/[:-]|\.\d{3}/g, '');
const shortDate = (date: Date) => date.toISOString().slice(0, 10).replace(/-/g, '');

const hmac = (key: crypto.BinaryLike, value: string) => crypto.createHmac('sha256', key).update(value).digest();
const sha256Hex = (value: crypto.BinaryLike) => crypto.createHash('sha256').update(value).digest('hex');

const encodeObjectKey = (key: string) => key.split('/').map(encodeURIComponent).join('/');
const encodeQueryValue = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);

const getSigningKey = (secretAccessKey: string, date: string, region: string) => {
  const dateKey = hmac(`AWS4${secretAccessKey}`, date);
  const regionKey = hmac(dateKey, region);
  const serviceKey = hmac(regionKey, 's3');
  return hmac(serviceKey, 'aws4_request');
};

const buildObjectUrl = (config: S3ImageStorageConfig, objectKey: string) => {
  const endpoint = config.endpoint.replace(/\/+$/, '');
  return new URL(`${endpoint}/${encodeURIComponent(config.bucket)}/${encodeObjectKey(objectKey)}`);
};

const signS3Request = ({
  config,
  method,
  objectKey,
  contentHash,
  contentType,
  now = new Date(),
}: {
  config: S3ImageStorageConfig;
  method: 'DELETE' | 'PUT';
  objectKey: string;
  contentHash: string;
  contentType?: string;
  now?: Date;
}) => {
  const url = buildObjectUrl(config, objectKey);
  const amzDate = awsDate(now);
  const date = shortDate(now);
  const credentialScope = `${date}/${config.region}/s3/aws4_request`;
  const headers: Record<string, string> = {
    host: url.host,
    'x-amz-content-sha256': contentHash,
    'x-amz-date': amzDate,
  };

  if (contentType) {
    headers['content-type'] = contentType;
  }

  const signedHeaders = Object.keys(headers).sort().join(';');
  const canonicalHeaders = Object.keys(headers)
    .sort()
    .map((key) => `${key}:${headers[key]}\n`)
    .join('');
  const canonicalRequest = [method, url.pathname, url.search.slice(1), canonicalHeaders, signedHeaders, contentHash].join('\n');
  const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Hex(canonicalRequest)].join('\n');
  const signature = crypto.createHmac('sha256', getSigningKey(config.secretAccessKey, date, config.region)).update(stringToSign).digest('hex');

  return {
    url,
    headers: {
      ...headers,
      Authorization: `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
    },
  };
};

export const getPublicObjectUrl = (config: S3ImageStorageConfig, objectKey: string) => {
  if (config.publicBaseUrl) {
    return `${config.publicBaseUrl.replace(/\/+$/, '')}/${encodeObjectKey(objectKey)}`;
  }

  return buildObjectUrl(config, objectKey).toString();
};

export const getSignedS3ObjectUrl = ({
  config,
  objectKey,
  expiresInSeconds = 900,
  now = new Date(),
}: {
  config: S3ImageStorageConfig;
  objectKey: string;
  expiresInSeconds?: number;
  now?: Date;
}) => {
  const url = buildObjectUrl(config, objectKey);
  const amzDate = awsDate(now);
  const date = shortDate(now);
  const credentialScope = `${date}/${config.region}/s3/aws4_request`;
  const credential = `${config.accessKeyId}/${credentialScope}`;
  const queryParams: Record<string, string> = {
    'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
    'X-Amz-Credential': credential,
    'X-Amz-Date': amzDate,
    'X-Amz-Expires': String(Math.min(Math.max(expiresInSeconds, 1), 604800)),
    'X-Amz-SignedHeaders': 'host',
  };
  const canonicalQuery = Object.keys(queryParams)
    .sort()
    .map((key) => `${encodeQueryValue(key)}=${encodeQueryValue(queryParams[key])}`)
    .join('&');
  const canonicalHeaders = `host:${url.host}\n`;
  const canonicalRequest = ['GET', url.pathname, canonicalQuery, canonicalHeaders, 'host', 'UNSIGNED-PAYLOAD'].join('\n');
  const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Hex(canonicalRequest)].join('\n');
  const signature = crypto.createHmac('sha256', getSigningKey(config.secretAccessKey, date, config.region)).update(stringToSign).digest('hex');

  url.search = `${canonicalQuery}&X-Amz-Signature=${signature}`;
  return url.toString();
};

export const putS3Object = async ({
  config,
  objectKey,
  content,
  contentType,
}: {
  config: S3ImageStorageConfig;
  objectKey: string;
  content: Buffer;
  contentType: string;
}) => {
  const contentHash = sha256Hex(content);
  const signed = signS3Request({ config, method: 'PUT', objectKey, contentHash, contentType });
  const response = await fetch(signed.url, {
    method: 'PUT',
    headers: signed.headers,
    body: content,
  });

  if (!response.ok) {
    const errorText = await response.text().catch(() => '');
    throw new Error(`Wasabi upload failed with ${response.status}${errorText ? `: ${errorText.slice(0, 300)}` : ''}`);
  }

  return getPublicObjectUrl(config, objectKey);
};

export const deleteS3Object = async ({
  config,
  objectKey,
}: {
  config: S3ImageStorageConfig;
  objectKey: string;
}) => {
  const signed = signS3Request({ config, method: 'DELETE', objectKey, contentHash: sha256Hex('') });
  const response = await fetch(signed.url, {
    method: 'DELETE',
    headers: signed.headers,
  });

  if (!response.ok && response.status !== 404) {
    const errorText = await response.text().catch(() => '');
    throw new Error(`Wasabi delete failed with ${response.status}${errorText ? `: ${errorText.slice(0, 300)}` : ''}`);
  }
};
