declare const __DEV__: boolean;

import * as FileSystem from 'expo-file-system';
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';

// Per-file mutex (M15): serializes read-modify-write saves so two concurrent
// callers can't both read the same base state and clobber each other's write.
const fileLocks = new Map<string, Promise<unknown>>();
function withFileLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
  const prev = fileLocks.get(key) || Promise.resolve();
  const next = prev.then(fn, fn);
  fileLocks.set(key, next.catch(() => undefined));
  return next;
}

import type {
  PhotoRecord,
  PersistPhotoOptions,
  StorageUsageReport,
  StorageState,
  PhotoType,
} from './types';
import {
  THUMBNAIL_MAX_DIMENSION,
  COMPRESSED_MAX_DIMENSION,
  COMPRESSED_JPEG_QUALITY,
} from './types';

const documentDirectory = FileSystem.documentDirectory || '';

export const photosDir = `${documentDirectory}pixiesprout-photos/`;
export const thumbnailsDir = `${documentDirectory}pixiesprout-photos/thumbnails/`;
export const compressedDir = `${documentDirectory}pixiesprout-photos/compressed/`;
export const recordsPath = `${documentDirectory}pixiesprout-garden/photo-records.json`;

const gardenDir = `${documentDirectory}pixiesprout-garden/`;
const savedPlantsPath = `${gardenDir}saved-plants.json`;
const gardenInfrastructurePath = `${gardenDir}garden-infrastructure.json`;

const storageAlertThresholds = {
  friendly: 1_000_000_000,
  stronger: 2_000_000_000,
  prominent: 5_000_000_000,
} as const;

const photoTypes: PhotoType[] = [
  'plant_id',
  'profile',
  'environment',
  'setup',
  'progress',
  'memory',
  'investigation',
  'kit',
  'wishlist',
];

type PhotoMetadata = {
  uri: string;
  fileSize?: number;
  width?: number;
  height?: number;
};

type MigrationCounter = {
  migrated: number;
  missing: number;
  skipped: number;
};

type MigrationResult = {
  value: unknown;
  changed: boolean;
};

function isDevRuntime() {
  return typeof __DEV__ !== 'undefined' && __DEV__;
}

function logDev(message: string, details: Record<string, unknown>) {
  if (isDevRuntime()) {
    console.log(message, details);
  }
}

function warnDev(message: string, details: Record<string, unknown>) {
  if (isDevRuntime()) {
    console.warn(message, details);
  }
}

function errorMessage(error: unknown) {
  return error instanceof Error ? error.message : 'unknown_error';
}

export function createPhotoId(prefix = 'photo') {
  return `${prefix}_${Date.now()}_${Math.round(Math.random() * 10000)}`;
}

export async function ensureDir(dir: string): Promise<void> {
  if (!FileSystem.documentDirectory || !dir) return;
  const info = await FileSystem.getInfoAsync(dir);
  if (!info.exists) {
    await FileSystem.makeDirectoryAsync(dir, { intermediates: true });
  }
}

function parentDir(path: string) {
  const index = path.lastIndexOf('/');
  return index >= 0 ? path.slice(0, index + 1) : '';
}

async function readJson<T>(path: string, fallback: T): Promise<T> {
  try {
    if (!FileSystem.documentDirectory) return fallback;
    const info = await FileSystem.getInfoAsync(path);
    if (!info.exists) return fallback;
    return JSON.parse(await FileSystem.readAsStringAsync(path)) as T;
  } catch (error) {
    warnDev('[PhotoStorage] storage read failed', { path, message: errorMessage(error) });
    return fallback;
  }
}

async function writeJson<T>(path: string, value: T): Promise<void> {
  if (!FileSystem.documentDirectory) return;
  await ensureDir(parentDir(path));
  // Atomic write (H8): write to a temp file then rename over the target. A
  // mid-write kill on a direct write corrupts the file and readJson silently
  // returns fallback {} / [], wiping data. Rename is atomic on Android.
  const tmpPath = `${path}.tmp`;
  await FileSystem.writeAsStringAsync(tmpPath, JSON.stringify(value));
  await FileSystem.moveAsync({ from: tmpPath, to: path });
}

function fileInfoSize(info: FileSystem.FileInfo) {
  return info.exists && 'size' in info && typeof info.size === 'number' ? info.size : undefined;
}

async function getFileSize(uri?: string, fallback = 0) {
  if (!uri) return 0;
  try {
    const info = await FileSystem.getInfoAsync(uri);
    return fileInfoSize(info) ?? fallback;
  } catch {
    return fallback;
  }
}

async function deleteFileIfExists(uri?: string) {
  if (!uri) return;
  try {
    const info = await FileSystem.getInfoAsync(uri);
    if (info.exists) {
      await FileSystem.deleteAsync(uri, { idempotent: true });
    }
  } catch (error) {
    warnDev('[PhotoStorage] file delete failed', { uri, message: errorMessage(error) });
  }
}

function isPermanentUri(uri: string) {
  return Boolean(FileSystem.documentDirectory && uri.includes(FileSystem.documentDirectory));
}

function isCachePhotoUri(uri: string) {
  return /ImagePicker|cache/i.test(uri);
}

function photoFileName(photoType: PhotoType) {
  return `${photoType}_${Date.now()}_${Math.round(Math.random() * 10000)}.jpg`;
}

function createEmptyPhotoTypeNumberMap(): Record<PhotoType, number> {
  return photoTypes.reduce<Record<PhotoType, number>>((acc, photoType) => {
    acc[photoType] = 0;
    return acc;
  }, {} as Record<PhotoType, number>);
}

function resizeLongestSide(metadata: PhotoMetadata, maxDimension: number) {
  if (metadata.width && metadata.height && metadata.height > metadata.width) {
    return { resize: { height: maxDimension } };
  }
  return { resize: { width: maxDimension } };
}

function activeStorageState(state: StorageState) {
  return state !== 'deleted' && state !== 'missing';
}

function ageBucket(createdAt: string): keyof StorageUsageReport['storageByAgeBucket'] {
  const createdTime = new Date(createdAt).getTime();
  const ageDays = Number.isFinite(createdTime)
    ? (Date.now() - createdTime) / (24 * 60 * 60 * 1000)
    : 0;

  if (ageDays <= 30) return 'last30Days';
  if (ageDays <= 90) return 'last90Days';
  if (ageDays <= 180) return 'last180Days';
  if (ageDays <= 365) return 'last365Days';
  return 'olderThan1Year';
}

function storageUrisForRecord(record: PhotoRecord) {
  const uris = new Set<string>();

  if (record.storageState === 'full') {
    uris.add(record.originalUri);
  }
  if (record.storageState === 'compressed' && record.compressedUri) {
    uris.add(record.compressedUri);
  }
  if (record.storageState === 'thumbnail_only' && record.thumbnailUri) {
    uris.add(record.thumbnailUri);
  }
  if (activeStorageState(record.storageState)) {
    if (record.activeUri) uris.add(record.activeUri);
    if (record.thumbnailUri) uris.add(record.thumbnailUri);
  }

  return Array.from(uris).filter(Boolean);
}

function createMissingRecord(
  tempUri: string,
  options: PersistPhotoOptions,
  photoId = createPhotoId(),
): PhotoRecord {
  return {
    photoId,
    plantId: options.plantId,
    relatedEntityId: options.relatedEntityId,
    relatedEntityType: options.relatedEntityType,
    photoType: options.photoType,
    description: options.description,
    originalUri: tempUri,
    activeUri: tempUri,
    createdAt: new Date().toISOString(),
    storageState: 'missing',
  };
}

function shouldInspectUriKey(keyHint?: string) {
  if (!keyHint) return false;
  const key = keyHint.toLowerCase();
  return key === 'uri' || key === 'image' || key.endsWith('uri') || key.endsWith('uris');
}

function inferPhotoTypeForKey(key: string, fallback: PhotoType): PhotoType {
  const lowerKey = key.toLowerCase();
  if (lowerKey.includes('environment')) return 'environment';
  if (lowerKey.includes('setup')) return 'setup';
  if (lowerKey.includes('kit') || lowerKey.includes('label')) return 'kit';
  if (lowerKey.includes('wishlist')) return 'wishlist';
  if (lowerKey.includes('event') || lowerKey.includes('progress')) return 'progress';
  if (lowerKey.includes('scan') || lowerKey.includes('identification')) return 'plant_id';
  if (lowerKey.includes('profile') || lowerKey === 'photouri' || lowerKey === 'image') return 'profile';
  return fallback;
}

async function copyPhotoToPermanent(uri: string, photoType: PhotoType) {
  await ensureDir(photosDir);
  const targetUri = `${photosDir}${photoFileName(photoType)}`;
  await FileSystem.copyAsync({ from: uri, to: targetUri });
  return targetUri;
}

async function migratePhotoUrisInValue(
  value: unknown,
  photoType: PhotoType,
  migrateUri: (uri: string, photoType: PhotoType) => Promise<string | null>,
  keyHint?: string,
): Promise<MigrationResult> {
  if (typeof value === 'string') {
    if (!shouldInspectUriKey(keyHint)) return { value, changed: false };
    const migratedUri = await migrateUri(value, photoType);
    return migratedUri && migratedUri !== value
      ? { value: migratedUri, changed: true }
      : { value, changed: false };
  }

  if (Array.isArray(value)) {
    let changed = false;
    const nextItems: unknown[] = [];

    for (const item of value) {
      const result = await migratePhotoUrisInValue(item, photoType, migrateUri, keyHint);
      changed = changed || result.changed;
      nextItems.push(result.value);
    }

    return { value: nextItems, changed };
  }

  if (value && typeof value === 'object') {
    let changed = false;
    const objectValue = value as Record<string, unknown>;
    const nextValue: Record<string, unknown> = { ...objectValue };

    for (const [key, item] of Object.entries(objectValue)) {
      const result = await migratePhotoUrisInValue(
        item,
        inferPhotoTypeForKey(key, photoType),
        migrateUri,
        key,
      );
      changed = changed || result.changed;
      nextValue[key] = result.value;
    }

    return { value: nextValue, changed };
  }

  return { value, changed: false };
}

export async function loadPhotoRecords(): Promise<Record<string, PhotoRecord>> {
  return readJson<Record<string, PhotoRecord>>(recordsPath, {});
}

export async function savePhotoRecords(records: Record<string, PhotoRecord>): Promise<void> {
  await writeJson(recordsPath, records);
}

export async function getPhotoRecord(photoId: string): Promise<PhotoRecord | null> {
  const records = await loadPhotoRecords();
  return records[photoId] || null;
}

export async function savePhotoRecord(record: PhotoRecord): Promise<void> {
  // Serialize the read-modify-write (M15) so concurrent saves don't clobber.
  await withFileLock('photo-records', async () => {
    const records = await loadPhotoRecords();
    await savePhotoRecords({
      ...records,
      [record.photoId]: record,
    });
  });
}

export async function getPhotoMetadata(photoUri: string): Promise<PhotoMetadata> {
  try {
    const [metadata, info] = await Promise.all([
      manipulateAsync(photoUri, [], { format: SaveFormat.JPEG }),
      FileSystem.getInfoAsync(photoUri),
    ]);

    return {
      uri: photoUri,
      fileSize: fileInfoSize(info),
      width: metadata.width,
      height: metadata.height,
    };
  } catch (error) {
    warnDev('[PhotoStorage] metadata read failed', { photoUri, message: errorMessage(error) });
    return { uri: photoUri };
  }
}

export async function persistPhoto(tempUri: string, options: PersistPhotoOptions): Promise<PhotoRecord> {
  const photoId = createPhotoId();

  try {
    await ensureDir(photosDir);

    const permanentUri = isPermanentUri(tempUri)
      ? tempUri
      : `${photosDir}${photoFileName(options.photoType)}`;

    if (!isPermanentUri(tempUri)) {
      await FileSystem.copyAsync({ from: tempUri, to: permanentUri });
    }

    const metadata = await getPhotoMetadata(permanentUri);
    const thumbnailUri = options.generateThumbnail === false
      ? undefined
      : await createThumbnail(permanentUri);

    const record: PhotoRecord = {
      photoId,
      plantId: options.plantId,
      relatedEntityId: options.relatedEntityId,
      relatedEntityType: options.relatedEntityType,
      photoType: options.photoType,
      description: options.description,
      originalUri: permanentUri,
      thumbnailUri,
      activeUri: permanentUri,
      createdAt: new Date().toISOString(),
      fileSize: metadata.fileSize,
      width: metadata.width,
      height: metadata.height,
      storageState: 'full',
    };

    await savePhotoRecord(record);
    logDev('[PhotoStorage] photo persisted', { photoId, photoType: options.photoType });
    return record;
  } catch (error) {
    warnDev('[PhotoStorage] persist failed', { tempUri, message: errorMessage(error) });
    return createMissingRecord(tempUri, options, photoId);
  }
}

export async function createThumbnail(photoUri: string): Promise<string | undefined> {
  try {
    await ensureDir(thumbnailsDir);

    const metadata = await getPhotoMetadata(photoUri);
    const thumbnail = await manipulateAsync(
      photoUri,
      [resizeLongestSide(metadata, THUMBNAIL_MAX_DIMENSION)],
      { compress: 0.5, format: SaveFormat.JPEG },
    );
    const thumbnailUri = `${thumbnailsDir}thumb_${Date.now()}_${Math.round(Math.random() * 10000)}.jpg`;

    await FileSystem.copyAsync({ from: thumbnail.uri, to: thumbnailUri });
    return thumbnailUri;
  } catch (error) {
    warnDev('[PhotoStorage] thumbnail creation failed', { photoUri, message: errorMessage(error) });
    return undefined;
  }
}

export async function compressOriginalPhoto(photoId: string): Promise<PhotoRecord | null> {
  const record = await getPhotoRecord(photoId);
  if (!record || !activeStorageState(record.storageState)) return record;

  try {
    await ensureDir(compressedDir);

    const metadata = await getPhotoMetadata(record.originalUri);
    const compressed = await manipulateAsync(
      record.originalUri,
      [resizeLongestSide(metadata, COMPRESSED_MAX_DIMENSION)],
      { compress: COMPRESSED_JPEG_QUALITY, format: SaveFormat.JPEG },
    );
    const compressedUri = `${compressedDir}compressed_${Date.now()}_${Math.round(Math.random() * 10000)}.jpg`;

    await FileSystem.copyAsync({ from: compressed.uri, to: compressedUri });
    const compressedInfo = await FileSystem.getInfoAsync(compressedUri);

    const updatedRecord: PhotoRecord = {
      ...record,
      compressedUri,
      activeUri: compressedUri,
      fileSize: fileInfoSize(compressedInfo) ?? record.fileSize,
      storageState: 'compressed',
    };

    // Save the updated record FIRST, then delete the original. If we deleted the
    // original before persisting the new record and crashed in between, the photo
    // would be permanently lost (H7). Worst case after this order: a stale original
    // lingers on disk, which is recoverable.
    await savePhotoRecord(updatedRecord);
    await deleteFileIfExists(record.originalUri);
    return updatedRecord;
  } catch (error) {
    warnDev('[PhotoStorage] compression failed', { photoId, message: errorMessage(error) });
    return null;
  }
}

export async function deleteOriginalPhoto(photoId: string): Promise<PhotoRecord | null> {
  const record = await getPhotoRecord(photoId);
  if (!record) return null;

  await deleteFileIfExists(record.originalUri);

  const updatedRecord: PhotoRecord = {
    ...record,
    activeUri: record.thumbnailUri || '',
    storageState: record.thumbnailUri ? 'thumbnail_only' : 'missing',
  };

  await savePhotoRecord(updatedRecord);
  return updatedRecord;
}

export async function deleteThumbnail(photoId: string): Promise<void> {
  const record = await getPhotoRecord(photoId);
  if (!record) return;

  await deleteFileIfExists(record.thumbnailUri);
  const { thumbnailUri: _thumbnailUri, ...updatedRecord } = record;
  await savePhotoRecord(updatedRecord);
}

/** Delete all photo records and files associated with a plant. */
export async function deletePhotosForPlant(plantId: string): Promise<number> {
  const records = await loadPhotoRecords();
  const toDelete = Object.values(records).filter(r => r.plantId === plantId);
  if (toDelete.length === 0) return 0;

  for (const record of toDelete) {
    await deleteFileIfExists(record.originalUri);
    await deleteFileIfExists(record.compressedUri);
    await deleteFileIfExists(record.thumbnailUri);
    delete records[record.photoId];
  }

  await savePhotoRecords(records);
  return toDelete.length;
}

export async function markPhotoAsCompressed(photoId: string): Promise<void> {
  const record = await getPhotoRecord(photoId);
  if (!record) return;

  await savePhotoRecord({
    ...record,
    activeUri: record.compressedUri || record.activeUri,
    storageState: 'compressed',
  });
}

export async function resolvePhotoUri(photoId: string): Promise<string | null> {
  const record = await getPhotoRecord(photoId);
  if (!record || record.storageState === 'deleted' || record.storageState === 'missing') {
    return null;
  }
  return record.activeUri || null;
}

export async function migrateCachePhotos(): Promise<MigrationCounter> {
  const counts: MigrationCounter = { migrated: 0, missing: 0, skipped: 0 };
  const migratedUris = new Map<string, string>();
  const missingUris = new Set<string>();

  const migrateUri = async (uri: string, photoType: PhotoType) => {
    if (!isCachePhotoUri(uri)) {
      counts.skipped += 1;
      return null;
    }

    if (migratedUris.has(uri)) return migratedUris.get(uri) || null;
    if (missingUris.has(uri)) return null;

    try {
      const info = await FileSystem.getInfoAsync(uri);
      if (!info.exists) {
        counts.missing += 1;
        missingUris.add(uri);
        return null;
      }

      const permanentUri = await copyPhotoToPermanent(uri, photoType);
      migratedUris.set(uri, permanentUri);
      counts.migrated += 1;
      return permanentUri;
    } catch (error) {
      counts.missing += 1;
      missingUris.add(uri);
      warnDev('[PhotoStorage] cache photo migration failed', { uri, message: errorMessage(error) });
      return null;
    }
  };

  const savedPlants = await readJson<unknown>(savedPlantsPath, null);
  const savedPlantsResult = await migratePhotoUrisInValue(savedPlants, 'profile', migrateUri);
  if (savedPlantsResult.changed) {
    await writeJson(savedPlantsPath, savedPlantsResult.value);
  }

  const gardenInfrastructure = await readJson<unknown>(gardenInfrastructurePath, null);
  const gardenInfrastructureResult = await migratePhotoUrisInValue(
    gardenInfrastructure,
    'environment',
    migrateUri,
  );
  if (gardenInfrastructureResult.changed) {
    await writeJson(gardenInfrastructurePath, gardenInfrastructureResult.value);
  }

  const records = await loadPhotoRecords();
  let recordsChanged = false;
  const nextRecords: Record<string, PhotoRecord> = {};

  for (const [photoId, record] of Object.entries(records)) {
    let nextRecord = { ...record };

    for (const key of ['originalUri', 'thumbnailUri', 'compressedUri', 'activeUri'] as const) {
      const uri = nextRecord[key];
      if (!uri) continue;

      const migratedUri = await migrateUri(uri, record.photoType);
      if (migratedUri) {
        nextRecord = { ...nextRecord, [key]: migratedUri };
        recordsChanged = true;
      } else if (missingUris.has(uri)) {
        nextRecord = { ...nextRecord, storageState: 'missing' };
        recordsChanged = true;
      }
    }

    nextRecords[photoId] = nextRecord;
  }

  if (recordsChanged) {
    await savePhotoRecords(nextRecords);
  }

  return counts;
}

export async function calculatePixieStorageUsage(): Promise<StorageUsageReport> {
  const records = await loadPhotoRecords();
  const storageByPhotoType = createEmptyPhotoTypeNumberMap();
  const countByPhotoType = createEmptyPhotoTypeNumberMap();
  const storageByPlant: Record<string, number> = {};
  const plantCounts: Record<string, number> = {};
  const storageByAgeBucket: StorageUsageReport['storageByAgeBucket'] = {
    last30Days: 0,
    last90Days: 0,
    last180Days: 0,
    last365Days: 0,
    olderThan1Year: 0,
  };
  let totalStorageBytes = 0;

  for (const record of Object.values(records)) {
    if (record.storageState === 'deleted') continue;

    let recordBytes = 0;
    for (const uri of storageUrisForRecord(record)) {
      const fallbackSize = uri === record.originalUri || uri === record.activeUri ? record.fileSize || 0 : 0;
      recordBytes += await getFileSize(uri, fallbackSize);
    }

    totalStorageBytes += recordBytes;
    storageByPhotoType[record.photoType] += recordBytes;
    countByPhotoType[record.photoType] += 1;
    storageByAgeBucket[ageBucket(record.createdAt)] += recordBytes;

    const plantId = record.plantId || 'unassigned';
    storageByPlant[plantId] = (storageByPlant[plantId] || 0) + recordBytes;
    plantCounts[plantId] = (plantCounts[plantId] || 0) + 1;
  }

  const largestPlantsByStorage = Object.entries(storageByPlant)
    .filter(([plantId]) => plantId !== 'unassigned')
    .map(([plantId, bytes]) => ({ plantId, bytes, count: plantCounts[plantId] || 0 }))
    .sort((a, b) => b.bytes - a.bytes)
    .slice(0, 5);

  return {
    totalStorageBytes,
    storageByPhotoType,
    storageByPlant,
    storageByAgeBucket,
    countByPhotoType,
    largestPlantsByStorage,
  };
}

export async function getStorageAlertLevel(): Promise<'none' | 'friendly' | 'stronger' | 'prominent'> {
  const usage = await calculatePixieStorageUsage();
  const total = usage.totalStorageBytes;

  if (total >= storageAlertThresholds.prominent) return 'prominent';
  if (total >= storageAlertThresholds.stronger) return 'stronger';
  if (total >= storageAlertThresholds.friendly) return 'friendly';
  return 'none';
}
