import sharp from 'sharp';
import { stat, mkdir } from 'fs/promises';
import { join, extname } from 'path';
import { logger } from '../../../utils/logger';

// Supported image extensions
export const ALLOWED_IMAGE_EXTENSIONS = [
  '.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.tiff', '.tif', '.heic', '.heif',
];

// Variant output directories (relative to /media/local/photos/)
const PHOTOS_BASE = '/media/local/photos';
const DIRS = {
  inbox: join(PHOTOS_BASE, 'inbox'),
  thumbnails: join(PHOTOS_BASE, 'thumbnails'),
  medium: join(PHOTOS_BASE, 'medium'),
  large: join(PHOTOS_BASE, 'large'),
  webp: join(PHOTOS_BASE, 'webp'),
};

export interface PhotoMetadata {
  width: number;
  height: number;
  orientation: 'H' | 'V' | 'S';
  fileSize: number;
  format: string;
  colorSpace: string | null;
  hasAlpha: boolean;
  dpi: number | null;
  // EXIF
  cameraMake: string | null;
  cameraModel: string | null;
  focalLength: string | null;
  aperture: string | null;
  shutterSpeed: string | null;
  iso: number | null;
  takenAt: Date | null;
  gpsLatitude: number | null;
  gpsLongitude: number | null;
}

export interface PhotoVariants {
  thumbnailPath: string;
  mediumPath: string;
  largePath: string;
  webpPath: string;
}

/**
 * Validate that a file is a real image (not just a renamed binary).
 * Returns metadata if valid, throws if not.
 */
export async function validateImage(filePath: string): Promise<sharp.Metadata> {
  try {
    const metadata = await sharp(filePath).metadata();
    if (!metadata.width || !metadata.height) {
      throw new Error('Image has no dimensions');
    }
    return metadata;
  } catch (error) {
    throw new Error(
      `Invalid image file: ${error instanceof Error ? error.message : 'unknown error'}`
    );
  }
}

/**
 * Extract metadata from an image file including EXIF data.
 */
export async function extractPhotoMetadata(filePath: string): Promise<PhotoMetadata> {
  const metadata = await sharp(filePath).metadata();

  if (!metadata.width || !metadata.height) {
    throw new Error('Image has no dimensions');
  }

  const fileStat = await stat(filePath);

  // Determine orientation
  let orientation: 'H' | 'V' | 'S' = 'H';
  if (metadata.width === metadata.height) {
    orientation = 'S';
  } else if (metadata.height > metadata.width) {
    orientation = 'V';
  }

  // Parse EXIF data
  const exif = metadata.exif ? parseExif(metadata) : null;

  return {
    width: metadata.width,
    height: metadata.height,
    orientation,
    fileSize: fileStat.size,
    format: metadata.format || extname(filePath).slice(1).toLowerCase(),
    colorSpace: metadata.space || null,
    hasAlpha: metadata.hasAlpha || false,
    dpi: metadata.density || null,
    cameraMake: exif?.make || null,
    cameraModel: exif?.model || null,
    focalLength: exif?.focalLength || null,
    aperture: exif?.aperture || null,
    shutterSpeed: exif?.shutterSpeed || null,
    iso: exif?.iso || null,
    takenAt: exif?.takenAt || null,
    gpsLatitude: exif?.gpsLatitude || null,
    gpsLongitude: exif?.gpsLongitude || null,
  };
}

interface ParsedExif {
  make: string | null;
  model: string | null;
  focalLength: string | null;
  aperture: string | null;
  shutterSpeed: string | null;
  iso: number | null;
  takenAt: Date | null;
  gpsLatitude: number | null;
  gpsLongitude: number | null;
}

/**
 * Parse EXIF data from sharp metadata.
 * sharp exposes raw EXIF buffer; we parse key IFD tags manually.
 */
function parseExif(metadata: sharp.Metadata): ParsedExif {
  const result: ParsedExif = {
    make: null,
    model: null,
    focalLength: null,
    aperture: null,
    shutterSpeed: null,
    iso: null,
    takenAt: null,
    gpsLatitude: null,
    gpsLongitude: null,
  };

  // sharp provides some EXIF-derived fields directly on metadata
  // For deeper EXIF, we'd need exif-reader, but sharp gives us basics
  // We'll use the raw exif buffer if available

  try {
    if (metadata.exif) {
      // Use dynamic import for exif-reader if available, otherwise skip
      // For now, rely on sharp's built-in metadata fields
      // sharp exposes: orientation, density (DPI)
      // Additional EXIF requires the exif-reader package

      // Try to parse raw EXIF with built-in support
      const exifBuffer = metadata.exif;
      if (exifBuffer && exifBuffer.length > 0) {
        try {
          // eslint-disable-next-line @typescript-eslint/no-var-requires
          const exifReader = require('exif-reader');
          const parsed = exifReader(exifBuffer);

          if (parsed.Image) {
            result.make = parsed.Image.Make || null;
            result.model = parsed.Image.Model || null;
          }
          if (parsed.Photo || parsed.Exif) {
            const photo = parsed.Photo || parsed.Exif || {};
            if (photo.FocalLength) result.focalLength = `${photo.FocalLength}mm`;
            if (photo.FNumber) result.aperture = `f/${photo.FNumber}`;
            if (photo.ExposureTime) {
              result.shutterSpeed = photo.ExposureTime < 1
                ? `1/${Math.round(1 / photo.ExposureTime)}s`
                : `${photo.ExposureTime}s`;
            }
            if (photo.ISOSpeedRatings) {
              result.iso = Array.isArray(photo.ISOSpeedRatings)
                ? photo.ISOSpeedRatings[0]
                : photo.ISOSpeedRatings;
            }
            if (photo.DateTimeOriginal) {
              result.takenAt = new Date(photo.DateTimeOriginal);
            }
          }
          if (parsed.GPSInfo || parsed.GPS) {
            const gps = parsed.GPSInfo || parsed.GPS || {};
            if (gps.GPSLatitude && gps.GPSLatitudeRef) {
              result.gpsLatitude = dmsToDecimal(gps.GPSLatitude, gps.GPSLatitudeRef);
            }
            if (gps.GPSLongitude && gps.GPSLongitudeRef) {
              result.gpsLongitude = dmsToDecimal(gps.GPSLongitude, gps.GPSLongitudeRef);
            }
          }
        } catch {
          // exif-reader not available or parse failed — that's fine
          logger.debug('EXIF parsing skipped (exif-reader not available or parse error)');
        }
      }
    }
  } catch (error) {
    logger.debug('EXIF extraction failed', { error });
  }

  return result;
}

/**
 * Convert DMS (degrees/minutes/seconds) array to decimal degrees.
 */
function dmsToDecimal(dms: number[], ref: string): number {
  if (!dms || dms.length < 3) return 0;
  let decimal = dms[0] + dms[1] / 60 + dms[2] / 3600;
  if (ref === 'S' || ref === 'W') decimal = -decimal;
  return Math.round(decimal * 1_000_000) / 1_000_000;
}

/**
 * Ensure all photo variant directories exist.
 */
export async function ensurePhotoDirs(): Promise<void> {
  for (const dir of Object.values(DIRS)) {
    await mkdir(dir, { recursive: true });
  }
}

/**
 * Generate all image variants (thumbnail, medium, large, webp).
 * Auto-orients using EXIF data, strips GPS from output variants.
 */
export async function generateVariants(
  sourcePath: string,
  photoId: number
): Promise<PhotoVariants> {
  await ensurePhotoDirs();

  const thumbPath = join(DIRS.thumbnails, `${photoId}_thumb.jpg`);
  const mediumPath = join(DIRS.medium, `${photoId}_medium.jpg`);
  const largePath = join(DIRS.large, `${photoId}_large.jpg`);
  const webpPath = join(DIRS.webp, `${photoId}.webp`);

  // Base pipeline: auto-orient (apply EXIF rotation) and strip metadata (privacy)
  const basePipeline = () => sharp(sourcePath).rotate().withMetadata({ orientation: undefined });

  // Generate all variants in parallel
  await Promise.all([
    // Thumbnail: 320px longest edge, JPEG q80
    basePipeline()
      .resize(320, 320, { fit: 'inside', withoutEnlargement: true })
      .jpeg({ quality: 80 })
      .toFile(thumbPath),

    // Medium: 800px longest edge, JPEG q85
    basePipeline()
      .resize(800, 800, { fit: 'inside', withoutEnlargement: true })
      .jpeg({ quality: 85 })
      .toFile(mediumPath),

    // Large: 1600px longest edge, JPEG q90
    basePipeline()
      .resize(1600, 1600, { fit: 'inside', withoutEnlargement: true })
      .jpeg({ quality: 90 })
      .toFile(largePath),

    // WebP: 1200px longest edge, WebP q82
    basePipeline()
      .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
      .webp({ quality: 82 })
      .toFile(webpPath),
  ]);

  logger.info(`Generated 4 variants for photo ${photoId}`);

  return {
    thumbnailPath: thumbPath,
    mediumPath,
    largePath,
    webpPath,
  };
}

/**
 * Get the inbox directory path for photo uploads.
 */
export function getPhotoInboxDir(): string {
  return DIRS.inbox;
}
