import { randomUUIDv7 } from 'bun';

import { getDb, pgGet, pgAll, pgRun } from '@/shared/db';
import { touchDevice } from '@/entities/device';
import { upsertAlbum, listAlbumsByDevice } from '@/entities/album';
import { upsertPhoto, listPhotos, markPhotoUploaded } from '@/entities/photo';
import type { Album } from '@/entities/album';
import type { Photo, PhotoFilter, PhotoMediaKind } from '@/entities/photo';
import { loadConfig } from '@/app/config';

import { getStorageBucket, writeBlob } from './storage';
import { enqueueThumbnailJob } from './thumbnail-worker';

export interface PhotoPayload {
  localIdentifier: string;
  mediaType: 'image' | 'video' | 'live_photo' | 'audio';
  width: number;
  height: number;
  fileSize?: number | null;
  durationSeconds?: number | null;
  capturedAt: string;
  modifiedAt?: string | null;
  originalFilename?: string | null;
  mimeType?: string | null;
  latitude?: number | null;
  longitude?: number | null;
  locationName?: string | null;
  isFavorite?: boolean | null;
  isHidden?: boolean | null;
  isScreenshot?: boolean | null;
  isSelfie?: boolean | null;
  isBurst?: boolean | null;
  burstIdentifier?: string | null;
  exif?: Record<string, unknown> | null;
  contentHash?: string | null;
}

export interface SyncPhotosPayload {
  photos: PhotoPayload[];
}

export interface AlbumPayload {
  localIdentifier: string;
  title: string;
  albumType: 'user' | 'smart' | 'shared' | 'system';
  photoCount?: number | null;
  photoLocalIdentifiers?: string[] | null;
  coverPhotoLocalIdentifier?: string | null;
  startDate?: string | null;
  endDate?: string | null;
  sortOrder?: number | null;
}

export interface SyncAlbumsPayload {
  albums: AlbumPayload[];
}

export async function ingestPhotos(
  deviceId: string,
  payload: SyncPhotosPayload,
): Promise<{ synced: number; needsUpload: string[]; skipped: number }> {
  const pool = getDb();
  let synced = 0;
  let skipped = 0;
  const needsUpload: string[] = [];

  for (const p of payload.photos) {
    const existing = await pgGet<{ id: string; storage_key: string | null }>(
      pool,
      'SELECT id, storage_key FROM macsync.photos WHERE device_id = $1 AND external_id = $2',
      [deviceId, p.localIdentifier],
    );

    if (existing?.storage_key) {
      skipped++;
      continue;
    }

    const id = existing?.id ?? randomUUIDv7();
    await upsertPhoto(pool, {
      id,
      deviceId,
      externalId: p.localIdentifier,
      filename: p.originalFilename ?? '',
      mediaType: p.mimeType ?? mimeFromFilename(p.originalFilename, p.mediaType),
      mediaKind: p.mediaType,
      mimeType: p.mimeType ?? null,
      width: p.width,
      height: p.height,
      duration: p.durationSeconds ?? null,
      takenAt: p.capturedAt,
      modifiedAt: p.modifiedAt ?? null,
      latitude: p.latitude ?? null,
      longitude: p.longitude ?? null,
      locationName: p.locationName ?? null,
      isFavorite: p.isFavorite ?? false,
      isHidden: p.isHidden ?? false,
      isScreenshot: p.isScreenshot ?? false,
      isSelfie: p.isSelfie ?? false,
      isBurst: p.isBurst ?? false,
      burstIdentifier: p.burstIdentifier ?? null,
      exif: p.exif ?? null,
      contentHash: p.contentHash ?? null,
    });

    needsUpload.push(p.localIdentifier);
    synced++;
  }

  await touchDevice(pool, deviceId);
  return { synced, needsUpload, skipped };
}

export async function ingestAlbums(deviceId: string, payload: SyncAlbumsPayload): Promise<{ synced: number }> {
  const pool = getDb();
  let synced = 0;

  for (const a of payload.albums) {
    await upsertAlbum(pool, {
      id: randomUUIDv7(),
      deviceId,
      externalId: a.localIdentifier,
      name: a.title,
    });
    synced++;
  }

  return { synced };
}

function storageKeyFor(deviceId: string, photoId: string, ext: string, capturedAt: string): string {
  const d = new Date(capturedAt);
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, '0');
  return `originals/${deviceId}/${y}/${m}/${photoId}.${ext}`;
}

/**
 * Resolve a MIME type from the filename extension. Falls back to a kind-based
 * guess if the extension is missing or unrecognised — preferable to defaulting
 * everything to image/jpeg or video/mp4 (which mis-stamps PNG/HEIC originals
 * and live_photos / audio assets).
 */
function mimeFromFilename(
  filename: string | undefined | null,
  kind: 'image' | 'video' | 'live_photo' | 'audio',
): string {
  const ext = filename?.split('.').pop()?.toLowerCase() ?? '';
  const map: Record<string, string> = {
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    jpe: 'image/jpeg',
    jfif: 'image/jpeg',
    png: 'image/png',
    gif: 'image/gif',
    webp: 'image/webp',
    heic: 'image/heic',
    heif: 'image/heif',
    avif: 'image/avif',
    tif: 'image/tiff',
    tiff: 'image/tiff',
    bmp: 'image/bmp',
    mov: 'video/quicktime',
    mp4: 'video/mp4',
    m4v: 'video/mp4',
    avi: 'video/x-msvideo',
    mpg: 'video/mpeg',
    mpeg: 'video/mpeg',
    m4a: 'audio/mp4',
    mp3: 'audio/mpeg',
    wav: 'audio/wav',
    aac: 'audio/aac',
  };
  if (ext && map[ext]) return map[ext];
  switch (kind) {
    case 'video':
      return 'video/mp4';
    case 'audio':
      return 'audio/mp4';
    case 'live_photo':
      return 'image/heic';
    case 'image':
    default:
      return 'image/jpeg';
  }
}

function extFromFilename(filename: string, mimeType: string): string {
  const fromFile = filename.split('.').pop()?.toLowerCase();
  if (fromFile && ['heic', 'heif', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'mov', 'mp4'].includes(fromFile)) {
    return fromFile;
  }
  const map: Record<string, string> = {
    'image/heic': 'heic', 'image/heif': 'heif', 'image/jpeg': 'jpg',
    'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp',
    'video/quicktime': 'mov', 'video/mp4': 'mp4',
  };
  return map[mimeType] ?? 'bin';
}

export async function storePhotoBlob(
  deviceId: string,
  localIdentifier: string,
  contentType: string,
  filename: string,
  stream: ReadableStream,
): Promise<{ photoId: string; storageKey: string; contentHash: string }> {
  const pool = getDb();
  const row = await pgGet<{ id: string; taken_at: string | null; media_kind: string | null }>(
    pool,
    'SELECT id, taken_at, media_kind FROM macsync.photos WHERE device_id = $1 AND external_id = $2',
    [deviceId, localIdentifier],
  );

  if (!row) throw new Error(`photo not found for localIdentifier: ${localIdentifier}`);

  const ext = extFromFilename(filename, contentType);
  const storageKey = storageKeyFor(deviceId, row.id, ext, row.taken_at ?? new Date().toISOString());

  // Buffer once so we can both hash and upload from the same bytes. storage.ts
  // already buffers internally for MinIO PUTs, so this doesn't add a copy in
  // practice — it just hoists the buffering up so the hash sees the same bytes.
  const buffer = Buffer.from(new Uint8Array(await new Response(stream).arrayBuffer()));
  const contentHash = new Bun.CryptoHasher('sha256').update(buffer).digest('hex');

  await writeBlob(storageKey, contentType, buffer);
  await markPhotoUploaded(pool, row.id, getStorageBucket(), storageKey, contentHash);

  await pgRun(
    pool,
    `UPDATE macsync.photos SET processing_status = 'processing', processing_error = NULL, updated_at = now() WHERE id = $1`,
    [row.id],
  );

  const mediaKind = (row.media_kind as PhotoMediaKind | null) ?? mediaKindFromMime(contentType);
  await enqueueThumbnailJob({
    photoId: row.id,
    deviceId,
    storageKey,
    mimeType: contentType,
    mediaKind,
  });

  return { photoId: row.id, storageKey, contentHash };
}

export function mediaKindFromMime(mime: string): PhotoMediaKind {
  if (mime.startsWith('video/')) return 'video';
  if (mime.startsWith('audio/')) return 'audio';
  return 'image';
}

export async function queryPhotos(filter: PhotoFilter): Promise<readonly Photo[]> {
  return listPhotos(getDb(), filter);
}

export async function queryAlbums(deviceId: string): Promise<readonly Album[]> {
  return listAlbumsByDevice(getDb(), deviceId);
}

export interface IphotoStats {
  totalPhotos: number;
  totalAlbums: number;
  pendingUpload: number;
  lastSyncAt: string | null;
}

export async function getIphotoStats(deviceId: string): Promise<IphotoStats> {
  const pool = getDb();
  const totalRows = await pgAll<{ n: number }>(
    pool,
    'SELECT COUNT(*) as n FROM macsync.photos WHERE device_id = $1',
    [deviceId],
  );
  const pendingRows = await pgAll<{ n: number }>(
    pool,
    'SELECT COUNT(*) as n FROM macsync.photos WHERE device_id = $1 AND storage_key IS NULL',
    [deviceId],
  );
  const albumRows = await pgAll<{ n: number }>(
    pool,
    'SELECT COUNT(*) as n FROM macsync.albums WHERE device_id = $1',
    [deviceId],
  );
  const lastRows = await pgAll<{ last: string | null }>(
    pool,
    'SELECT MAX(synced_at) as last FROM macsync.photos WHERE device_id = $1',
    [deviceId],
  );
  const totalRow = totalRows[0];
  const pendingRow = pendingRows[0];
  const albumRow = albumRows[0];
  const lastRow = lastRows[0];
  return {
    totalPhotos: totalRow?.n ?? 0,
    totalAlbums: albumRow?.n ?? 0,
    pendingUpload: pendingRow?.n ?? 0,
    lastSyncAt: lastRow?.last ?? null,
  };
}

export interface PendingUploadPhoto {
  id: string;
  localIdentifier: string;
  mediaType: string;
  filename: string;
  takenAt: string | null;
  syncedAt: string;
}

export async function listPendingUploads(deviceId: string, limit = 100): Promise<readonly PendingUploadPhoto[]> {
  const pool = getDb();
  const rows = await pgAll<{
    id: string;
    external_id: string;
    media_type: string;
    filename: string;
    taken_at: string | null;
    synced_at: string;
  }>(
    pool,
    'SELECT id, external_id, media_type, filename, taken_at, synced_at FROM macsync.photos WHERE device_id = $1 AND storage_key IS NULL ORDER BY taken_at ASC NULLS LAST LIMIT $2',
    [deviceId, limit],
  );
  return rows.map((r) => ({
    id: r.id,
    localIdentifier: r.external_id,
    mediaType: r.media_type,
    filename: r.filename,
    takenAt: r.taken_at,
    syncedAt: r.synced_at,
  }));
}
