import { stat } from 'node:fs/promises';
import { resolve, sep } from 'node:path';
import { homedir } from 'node:os';

// All processing lives here (the platform is just a proxy). Files live under the owner's home — single
// super user, so HOME_DIR. Paths from the app are home-relative (e.g. "Music/Artist/Album/track.mp3"),
// exactly like file-browser /raw.
const ROOT_DIR = homedir();

const CONTENT_TYPES: Record<string, string> = {
  mp3: 'audio/mpeg',
  m4a: 'audio/mp4',
  mp4: 'audio/mp4',
  aac: 'audio/aac',
  flac: 'audio/flac',
  wav: 'audio/wav',
  ogg: 'audio/ogg',
  opus: 'audio/opus',
  wma: 'audio/x-ms-wma',
};

// Probe duration once per file (keyed by absolute path + mtime) — the player makes many range requests
// per track, and we don't want to shell out to ffprobe on each one.
const durationCache = new Map<string, number>();

async function probeDuration(absPath: string, mtimeMs: number): Promise<number | undefined> {
  const key = `${absPath}:${mtimeMs}`;
  const cached = durationCache.get(key);
  if (cached !== undefined) return cached;
  try {
    const proc = Bun.spawn(
      [
        'ffprobe',
        '-v',
        'error',
        '-show_entries',
        'format=duration',
        '-of',
        'default=noprint_wrappers=1:nokey=1',
        absPath,
      ],
      { stdout: 'pipe', stderr: 'ignore' },
    );
    const out = (await new Response(proc.stdout).text()).trim();
    await proc.exited;
    const d = parseFloat(out);
    if (Number.isFinite(d) && d > 0) {
      durationCache.set(key, d);
      return d;
    }
  } catch {
    /* ffprobe missing or failed — no duration header */
  }
  return undefined;
}

/** Resolve a home-relative path within ROOT_DIR; null if it escapes (traversal). */
function resolveWithinRoot(relPath: string): string | null {
  const clean = relPath.replace(/^\/+/, '');
  const abs = resolve(ROOT_DIR, clean);
  if (abs !== ROOT_DIR && !abs.startsWith(ROOT_DIR + sep)) return null;
  return abs;
}

/** Serve an audio file with byte-range support + an X-Audio-Duration header (ffprobe-derived). */
export async function streamAudioFile(relPath: string, rangeHeader: string | null): Promise<Response> {
  const absPath = resolveWithinRoot(relPath);
  if (!absPath) return new Response('Invalid path', { status: 400 });

  let s;
  try {
    s = await stat(absPath);
  } catch {
    return new Response('Not found', { status: 404 });
  }
  if (!s.isFile()) return new Response('Not a file', { status: 404 });

  const total = s.size;
  const ext = absPath.slice(absPath.lastIndexOf('.') + 1).toLowerCase();
  const contentType = CONTENT_TYPES[ext] ?? 'application/octet-stream';
  const duration = await probeDuration(absPath, s.mtimeMs);
  const file = Bun.file(absPath);

  const baseHeaders: Record<string, string> = {
    'Content-Type': contentType,
    'Accept-Ranges': 'bytes',
    ...(duration ? { 'X-Audio-Duration': String(duration) } : {}),
  };

  if (rangeHeader) {
    const m = rangeHeader.match(/bytes=(\d*)-(\d*)/);
    if (m) {
      const start = m[1] ? parseInt(m[1], 10) : 0;
      const end = m[2] ? parseInt(m[2], 10) : total - 1;
      if (Number.isNaN(start) || start < 0 || end >= total || start > end) {
        return new Response('Invalid range', { status: 416, headers: { 'Content-Range': `bytes */${total}` } });
      }
      return new Response(file.slice(start, end + 1), {
        status: 206,
        headers: {
          ...baseHeaders,
          'Content-Range': `bytes ${start}-${end}/${total}`,
          'Content-Length': String(end - start + 1),
        },
      });
    }
  }

  return new Response(file, { status: 200, headers: { ...baseHeaders, 'Content-Length': String(total) } });
}
