import { matchesRuleSet, resolveLabel, resolveSmartAlbum } from '../lib/smartAlbums';
import type { LabelAliases } from '../lib/smartAlbums';
import type { Album, MediaItem } from '../types';
import type { GalleryState, GridFilter } from './store';

/** Items that are not in the recycle bin and not hidden. */
export function liveMedia(media: MediaItem[]): MediaItem[] {
  return media.filter((m) => !m.deletedAt && !m.hidden);
}

export function trashedMedia(media: MediaItem[]): MediaItem[] {
  return media
    .filter((m) => m.deletedAt)
    .sort((a, b) => (b.deletedAt ?? 0) - (a.deletedAt ?? 0));
}

export function albumById(albums: Album[], id: string): Album | undefined {
  return albums.find((a) => a.id === id);
}

/** Resolve the members of any album (smart albums are computed live). */
export function albumMedia(album: Album, media: MediaItem[], aliases: LabelAliases = {}): MediaItem[] {
  if (album.kind === 'smart' && album.ruleSet) {
    const ids = new Set(resolveSmartAlbum(album, media, aliases));
    return liveMedia(media)
      .filter((m) => ids.has(m.id))
      .sort((a, b) => b.takenAt - a.takenAt);
  }
  const order = new Map(album.mediaIds.map((id, i) => [id, i]));
  return liveMedia(media)
    .filter((m) => order.has(m.id))
    .sort((a, b) => (order.get(a.id)! - order.get(b.id)!));
}

function applyGridFilter(items: MediaItem[], filter: GridFilter): MediaItem[] {
  switch (filter) {
    case 'favourites': return items.filter((m) => m.favorite);
    case 'edited': return items.filter((m) => Boolean(m.edits));
    case 'photos': return items.filter((m) => m.kind === 'image');
    case 'videos': return items.filter((m) => m.kind === 'video');
    case 'screenshots': return items.filter((m) => m.source === 'screenshot');
    case 'not-in-album': return items.filter((m) => m.albumIds.length === 0);
    default: return items;
  }
}

/** Full-text-ish search across the most useful fields. */
export function searchMedia(items: MediaItem[], query: string): MediaItem[] {
  const q = query.trim().toLowerCase();
  if (!q) return items;
  const tokens = q.split(/\s+/).filter(Boolean);
  return items.filter((m) => {
    const haystack = [
      m.name,
      m.source,
      m.kind,
      m.caption ?? '',
      m.note ?? '',
      m.ocrText ?? '',
      m.location?.place ?? '',
      m.location?.formatted ?? '',
      m.location?.city ?? '',
      m.location?.state ?? '',
      m.location?.postcode ?? '',
      m.location?.country ?? '',
      ...m.tags,
      ...m.objectLabels,
    ]
      .join(' ')
      .toLowerCase();
    // Every token must match somewhere (AND semantics).
    return tokens.every((t) => haystack.includes(t));
  });
}

/**
 * The media shown for the current view, after applying trash exclusion,
 * grid filter, search and object-focus. Returns reverse-chronological order.
 */
export function mediaForView(state: GalleryState): MediaItem[] {
  const { media, albums, view, gridFilter, searchQuery, objectFocus, tagFocus, personFocus, searchPreset, semanticResults, recentlyViewed, labelAliases } =
    state;
  let items: MediaItem[];
  // When a search ranks results (keyword + semantic), preserve that order
  // instead of re-sorting chronologically.
  let ranked = false;

  // Quick search presets (from the search dropdown) take precedence on the search view.
  if (view === 'search' && searchPreset) {
    const live = liveMedia(media);
    if (searchPreset === 'edited') {
      // Edited = has an edit timestamp (baked crops/AI) or a live edit stack (filters/markup).
      return live
        .filter((m) => m.editedAt || m.edits)
        .sort((a, b) => (b.editedAt ?? 0) - (a.editedAt ?? 0));
    }
    if (searchPreset === 'added') {
      return [...live].sort((a, b) => b.importedAt - a.importedAt);
    }
    // viewed
    const order = new Map(recentlyViewed.map((id, i) => [id, i]));
    return live.filter((m) => order.has(m.id)).sort((a, b) => order.get(a.id)! - order.get(b.id)!);
  }

  if (view === 'recently-deleted') {
    items = trashedMedia(media);
  } else if (view === 'duplicates') {
    items = liveMedia(media);
  } else if (view.startsWith('album:') || view.startsWith('sys:')) {
    const album = albumById(albums, view);
    items = album ? albumMedia(album, media, labelAliases) : [];
  } else if (view === 'favourites') {
    items = liveMedia(media).filter((m) => m.favorite);
  } else if (view === 'recently-saved') {
    items = liveMedia(media).filter((m) => m.source === 'download' || m.source === 'social');
  } else if (view === 'videos') {
    items = liveMedia(media).filter((m) => m.kind === 'video');
  } else if (view === 'screenshots') {
    items = liveMedia(media).filter((m) => m.source === 'screenshot');
  } else {
    items = liveMedia(media);
  }

  if (view !== 'recently-deleted') {
    items = applyGridFilter(items, gridFilter);
  }
  if (objectFocus) {
    // Match on the resolved (renamed) label so focusing a renamed object still
    // surfaces photos whose stored label is the original detector label.
    const focus = resolveLabel(objectFocus, labelAliases);
    items = items.filter((m) => m.objectLabels.some((l) => resolveLabel(l, labelAliases) === focus));
  }
  if (tagFocus) {
    items = items.filter((m) => m.tags.includes(tagFocus));
  }
  if (personFocus) {
    items = items.filter((m) => m.personIds.includes(personFocus));
  }
  if (searchQuery && (view === 'search' || view === 'library')) {
    const keyword = searchMedia(items, searchQuery);
    // Blend in semantic ("looks like") matches after the keyword hits, in
    // similarity order, skipping any already matched by keyword.
    if (semanticResults && semanticResults.length) {
      const inView = new Map(items.map((m) => [m.id, m]));
      const kwIds = new Set(keyword.map((m) => m.id));
      const semantic = semanticResults
        .filter((id) => !kwIds.has(id) && inView.has(id))
        .map((id) => inView.get(id)!);
      items = [...keyword, ...semantic];
    } else {
      items = keyword;
    }
    ranked = true;
  }

  // Default reverse-chronological unless an album imposed its own order or the
  // results are relevance-ranked by search.
  if (!view.startsWith('album:') && !ranked) {
    items = [...items].sort((a, b) => b.takenAt - a.takenAt);
  }
  return items;
}

/** Distinct object labels across the live library (for AI auto-albums), resolved
 * through the user's rename map so renamed tags collapse into one bucket. */
export function objectLabelCounts(media: MediaItem[], aliases: LabelAliases = {}): Map<string, number> {
  const counts = new Map<string, number>();
  for (const m of liveMedia(media)) {
    // De-dupe per item after resolving so two labels renamed to the same value
    // only count that photo once.
    for (const label of new Set(m.objectLabels.map((l) => resolveLabel(l, aliases)))) {
      counts.set(label, (counts.get(label) ?? 0) + 1);
    }
  }
  return counts;
}

/** Items with a geolocation (for the Map view). Any source — incl. screenshots. */
export function locatedMedia(media: MediaItem[]): MediaItem[] {
  return liveMedia(media).filter((m) => m.location);
}

export interface LocationCluster {
  /** Centroid of the grouped photos. */
  lat: number;
  lng: number;
  /** Photos at this location, most-recent first (first = pin cover). */
  items: MediaItem[];
}

/**
 * Group located media into pins by rounding coordinates to `precision` decimals
 * (1 ≈ 11 km, 2 ≈ 1.1 km). The Map view raises precision as you zoom in, so
 * clusters split apart — macOS-style.
 */
export function clusterByLocation(items: MediaItem[], precision = 1): LocationCluster[] {
  const groups = new Map<string, MediaItem[]>();
  for (const m of items) {
    if (!m.location) continue;
    const key = `${m.location.lat.toFixed(precision)},${m.location.lng.toFixed(precision)}`;
    const arr = groups.get(key);
    if (arr) arr.push(m);
    else groups.set(key, [m]);
  }
  return [...groups.values()].map((arr) => {
    const lat = arr.reduce((s, m) => s + m.location!.lat, 0) / arr.length;
    const lng = arr.reduce((s, m) => s + m.location!.lng, 0) / arr.length;
    const sorted = [...arr].sort((a, b) => b.takenAt - a.takenAt);
    return { lat, lng, items: sorted };
  });
}

export { matchesRuleSet };
