import { FastifyInstance } from 'fastify';
import { createReadStream } from 'fs';
import { stat } from 'fs/promises';
import path from 'path';
import { DbService } from '../services/db.service.js';

// Music library root on disk. The worker scanner writes absolute file paths into
// tracks.path rooted here; the backend container must mount the same path so they
// resolve. Path-traversal guard below verifies the resolved file stays inside.
const MUSIC_DIR = path.resolve(process.env.MUSIC_DIR || '/mnt/hdd1/media/Music');

const CONTENT_TYPES: Record<string, string> = {
  '.mp3': 'audio/mpeg',
  '.flac': 'audio/flac',
  '.m4a': 'audio/mp4',
  '.wav': 'audio/wav',
  '.ogg': 'audio/ogg',
};

function contentTypeFor(filePath: string): string {
  return CONTENT_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
}

// True when resolvedPath is the music root itself or a descendant of it.
function isInsideRoot(resolvedPath: string, root: string): boolean {
  return resolvedPath === root || resolvedPath.startsWith(root + path.sep);
}

export default async function streamRoutes(
  fastify: FastifyInstance,
  options: { dbService: DbService }
) {
  const { dbService } = options;

  fastify.get('/tracks/:id/stream', async (request, reply) => {
    const { id } = request.params as { id: string };

    const track = await dbService.getTrackById(id);
    if (!track) {
      return reply.code(404).send({ error: 'Track not found' });
    }

    // SECURITY: resolve the path and confirm it stays within MUSIC_DIR. This
    // rejects relative paths, symlink-style escapes and any path outside root.
    const resolvedPath = path.resolve(track.path);
    if (!isInsideRoot(resolvedPath, MUSIC_DIR)) {
      return reply.code(403).send({ error: 'Forbidden' });
    }

    let fileSize: number;
    try {
      const stats = await stat(resolvedPath);
      if (!stats.isFile()) {
        return reply.code(404).send({ error: 'File not found' });
      }
      fileSize = stats.size;
    } catch (err: any) {
      if (err && err.code === 'ENOENT') {
        // File missing on disk; the integrity worker would flag this track MISSING.
        return reply.code(404).send({ error: 'File not found on disk' });
      }
      throw err;
    }

    const contentType = contentTypeFor(resolvedPath);
    const rangeHeader = request.headers.range;

    // No Range header: stream the whole file with a 200.
    if (!rangeHeader) {
      reply
        .code(200)
        .header('Content-Type', contentType)
        .header('Content-Length', fileSize)
        .header('Accept-Ranges', 'bytes');
      const stream = createReadStream(resolvedPath);
      stream.on('error', (err) => {
        request.log.error(err);
        reply.raw.destroy(err);
      });
      return reply.send(stream);
    }

    // Parse "bytes=start-end". Either bound may be omitted.
    const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
    if (!match || (match[1] === '' && match[2] === '')) {
      return reply
        .code(416)
        .header('Content-Range', `bytes */${fileSize}`)
        .send({ error: 'Invalid range' });
    }

    let start: number;
    let end: number;
    if (match[1] === '') {
      // suffix range: last N bytes
      const suffixLength = parseInt(match[2], 10);
      if (suffixLength <= 0) {
        return reply
          .code(416)
          .header('Content-Range', `bytes */${fileSize}`)
          .send({ error: 'Unsatisfiable range' });
      }
      start = Math.max(fileSize - suffixLength, 0);
      end = fileSize - 1;
    } else {
      start = parseInt(match[1], 10);
      end = match[2] === '' ? fileSize - 1 : parseInt(match[2], 10);
    }

    if (end > fileSize - 1) end = fileSize - 1;

    if (
      Number.isNaN(start) ||
      Number.isNaN(end) ||
      start > end ||
      start < 0 ||
      start >= fileSize
    ) {
      return reply
        .code(416)
        .header('Content-Range', `bytes */${fileSize}`)
        .send({ error: 'Unsatisfiable range' });
    }

    const chunkSize = end - start + 1;
    reply
      .code(206)
      .header('Content-Type', contentType)
      .header('Content-Range', `bytes ${start}-${end}/${fileSize}`)
      .header('Accept-Ranges', 'bytes')
      .header('Content-Length', chunkSize);

    const stream = createReadStream(resolvedPath, { start, end });
    stream.on('error', (err) => {
      request.log.error(err);
      reply.raw.destroy(err);
    });
    return reply.send(stream);
  });
}
