/**
 * Secure uploads in two steps:
 *
 *   1. POST /api/files/sign   -> server records a `pending` attachment row and
 *                                returns a short-lived, HMAC-signed upload URL.
 *   2. PUT  <signed url>      -> bytes are streamed to the storage driver and
 *                                the row flips to `uploaded`.
 *
 * Nothing is writable without a signature the server issued, the MIME type and
 * size ceiling are decided server-side, and an abandoned step 2 leaves only a
 * harmless `pending` row.
 */
import { one, query } from '../../db/pool.js';
import { env } from '../../config/env.js';
import { buildStorageKey, storage } from '../../storage/index.js';
import { signFileUrl } from '../../storage/signing.js';
import { badRequest, notFound, tooLarge } from '../../utils/errors.js';
import type { AttachmentDTO, SenderType } from '../types.js';

/** Deny-list of types that are dangerous to hand back to a browser. */
const BLOCKED_MIME = [
  'application/x-msdownload',
  'application/x-msdos-program',
  'application/x-sh',
  'application/x-executable',
  'text/html',        // stored HTML would be a stored-XSS vector on the file origin
  'image/svg+xml',    // SVG can carry script
];

/** Characters that are unsafe in a filename on common filesystems. */
const RESERVED_FILENAME_CHARS = '<>:"|?*';

export interface SignUploadInput {
  conversationId: string;
  uploaderType: SenderType;
  uploaderId?: string | null;
  filename: string;
  mimeType: string;
  size: number;
}

export interface SignedUpload {
  attachmentId: string;
  uploadUrl: string;
  maxBytes: number;
  expiresAt: string;
}

export async function signUpload(input: SignUploadInput): Promise<SignedUpload> {
  const filename = sanitizeFilename(input.filename);
  const mimeType = (input.mimeType || 'application/octet-stream').toLowerCase();

  if (!Number.isFinite(input.size) || input.size <= 0) throw badRequest('A positive file size is required');
  if (input.size > env.MAX_UPLOAD_BYTES) {
    throw tooLarge(`Files must be ${Math.floor(env.MAX_UPLOAD_BYTES / 1024 / 1024)} MB or smaller`);
  }
  if (BLOCKED_MIME.includes(mimeType)) throw badRequest(`Files of type ${mimeType} are not allowed`);

  const row = await one(
    `INSERT INTO attachments
       (conversation_id, uploader_type, uploader_id, storage_key, original_name, mime_type, size_bytes, status)
     VALUES ($1,$2,$3,'',$4,$5,$6,'pending') RETURNING id`,
    [input.conversationId, input.uploaderType, input.uploaderId ?? null, filename, mimeType, input.size],
  );
  if (!row) throw badRequest('Could not create attachment');

  const key = buildStorageKey(row.id, filename);
  await query(`UPDATE attachments SET storage_key = $2 WHERE id = $1`, [row.id, key]);

  const { exp, sig } = signFileUrl('PUT', key, env.FILE_URL_TTL);
  const params = new URLSearchParams({ key, exp: String(exp), sig, attachmentId: row.id });

  return {
    attachmentId: row.id,
    uploadUrl: `${env.PUBLIC_URL}/api/files/upload?${params.toString()}`,
    maxBytes: env.MAX_UPLOAD_BYTES,
    expiresAt: new Date(exp * 1000).toISOString(),
  };
}

/** Called by the upload route once bytes have landed in storage. */
export async function markUploaded(attachmentId: string, bytes: number): Promise<AttachmentDTO> {
  const row = await one(
    `UPDATE attachments SET status = 'uploaded', size_bytes = $2 WHERE id = $1 RETURNING *`,
    [attachmentId, bytes],
  );
  if (!row) throw notFound('Attachment not found');
  return toAttachmentDTO(row);
}

export async function markFailed(attachmentId: string): Promise<void> {
  await query(`UPDATE attachments SET status = 'failed' WHERE id = $1`, [attachmentId]);
}

export async function getAttachment(id: string) {
  const row = await one(`SELECT * FROM attachments WHERE id = $1`, [id]);
  if (!row) throw notFound('Attachment not found');
  return row;
}

export async function toAttachmentDTO(row: any): Promise<AttachmentDTO> {
  return {
    id: row.id,
    name: row.original_name,
    mimeType: row.mime_type,
    size: Number(row.size_bytes),
    url: await storage.downloadUrl(row.storage_key, env.FILE_URL_TTL, row.original_name).catch(() => null),
    isImage: String(row.mime_type).startsWith('image/'),
  };
}

/**
 * Strips directory components, control characters and filesystem-reserved
 * characters, so an uploaded name can never escape the storage root.
 */
export function sanitizeFilename(name: string): string {
  const base = String(name).split(/[\\/]/).pop() ?? 'file';
  const clean = Array.from(base)
    .filter((ch) => {
      const code = ch.codePointAt(0) ?? 0;
      return code > 31 && code !== 127 && !RESERVED_FILENAME_CHARS.includes(ch);
    })
    .join('')
    .trim();
  return (clean || 'file').slice(0, 200);
}
