import { z } from '@zod/zod/mini'

/** Latitude: max/min (90 / -90) */
export const Lat = z.number().check(z.maximum(90), z.minimum(-90))
export type Lat = z.infer<typeof Lat>

/** Longitude: max/min (180 / -180) */
export const Lng = z.number().check(z.maximum(180), z.minimum(-180))
export type Lng = z.infer<typeof Lng>

export const SearchHistoryEntry = z.object({
  query: z.string(),
  timestamp: z.string(),
})
export type SearchHistoryEntry = z.infer<typeof SearchHistoryEntry>

export const BookmarkAddress = z.object({
  city: z.optional(z.string()),
  cityBlock: z.optional(z.string()),
  cityDistrict: z.optional(z.string()),
  country: z.optional(z.string()),
  countryCode: z.optional(z.string()),
  displayText: z.optional(z.string()),
  historic: z.optional(z.string()),
  houseNumber: z.optional(z.string()),
  postcode: z.optional(z.string()),
  region: z.optional(z.string()),
  road: z.optional(z.string()),
  suburb: z.optional(z.string()),
  state: z.optional(z.string()),
  type: z.optional(z.string()),
  'ISO3166-2-lvl4': z.optional(z.string()),
  'ISO3166-2-lvl6': z.optional(z.string()),
})
export type BookmarkAddress = z.infer<typeof BookmarkAddress>

export const BookmarkProperties = z.object({
  address: z.optional(BookmarkAddress),
  addressType: z.optional(z.string()),
  displayName: z.optional(z.string()),
  description: z.optional(z.string()),
  links: z.optional(z.record(z.string(), z.string())),
  importance: z.optional(z.number()),
  name: z.optional(z.string()),
  nominatimCategory: z.optional(z.string()),
  nominatimId: z.optional(z.number()),
  nominatimPlaceRank: z.optional(z.number()),
  nominatimType: z.optional(z.string()),
  osmId: z.optional(z.number()),
  osmType: z.optional(z.string()),
})
export type BookmarkProperties = z.infer<typeof BookmarkProperties>

export const BlobRef = z.object({
  hash: z.string(),
  mime: z.string(),
  size: z.number(),
})
export type BlobRef = z.infer<typeof BlobRef>

export const Bookmark = z.object({
  id: z.string(),
  type: z.literal('Feature'),
  geometry: z.object({
    type: z.literal('Point'),
    // GeoJSON coordinate order: [lng, lat]
    coordinates: z.tuple([Lng, Lat]),
  }),
  properties: z._default(BookmarkProperties, {}),
  /**
   * Ids of the {@link BookmarkList}s this bookmark belongs to. Named
   * `categories` for historical reasons — it's a persisted, synced field, so
   * renaming it would need a store migration that also has to reach devices
   * holding the old shape. Everything above this layer says "list".
   */
  categories: z._default(z.array(z.string()), []),
  zoom: z._default(z.number(), 12),
  images: z._default(z.array(BlobRef), []),
  visited: z._default(z.boolean(), false),
  favorite: z._default(z.boolean(), false),
  createdAt: z.string(),
  updatedAt: z.string(),
})
export type Bookmark = z.infer<typeof Bookmark>

/** Resolves a human-readable name for display, falling through available fields. */
export function bookmarkDisplayName(b: Bookmark): string {
  return (
    b.properties?.name ??
      b.properties?.displayName ??
      b.properties?.address?.displayText ??
      `${(b.geometry?.coordinates?.[1] || 0).toFixed(4)}, ${
        (b.geometry?.coordinates?.[0] || 0).toFixed(4)
      }`
  )
}

/**
 * A named list of bookmarks — the app's only grouping concept. Stored under
 * the `bookmarkCollections` key, which predates the rename and stays put so
 * existing local and synced data keeps loading.
 */
export const BookmarkList = z.object({
  id: z.string(),
  name: z.string(),
  color: z.optional(z.string()),
  icon: z.optional(z.string()),
  createdAt: z.string(),
  updatedAt: z.string(),
})
export type BookmarkList = z.infer<typeof BookmarkList>

/**
 * The public projection of one location inside a shared list — deliberately
 * *not* the stored {@link Bookmark}.
 *
 * Publishing the stored document would expose the ids of the publisher's
 * other lists (`categories`), blob refs the share doesn't serve, geocoder
 * internals (`nominatimId`, `osmId`, place ranks) and internal ids and
 * timestamps. It would also tie the public wire format to the internal
 * schema, so any future change to `Bookmark` would silently change what
 * every live link serves.
 */
export const SharedBookmark = z.object({
  /** Display name, resolved at publish time via {@link bookmarkDisplayName}. */
  name: z.string(),
  /** GeoJSON coordinate order: [lng, lat]. */
  coordinates: z.tuple([Lng, Lat]),
  address: z.optional(z.string()),
  description: z.optional(z.string()),
  links: z.optional(z.record(z.string(), z.string())),
  /**
   * Kept because the single-location viewer already surfaces it — on a shared
   * trip list "I've been to these" is usually the point, not a leak.
   */
  visited: z._default(z.boolean(), false),
  /** Same reasoning as {@link SharedBookmark.visited} — a shared favorite is
   * meant to be seen, not stripped on publish. */
  favorite: z._default(z.boolean(), false),
  /** Preserves the publisher's framing when a recipient imports the list. */
  zoom: z._default(z.number(), 12),
  /**
   * Only populated for single-location shares, where CivShare serves the
   * referenced blobs from that share's own scoped public route. List shares
   * leave this empty: a recipient importing the list would end up with refs
   * to bytes they can't fetch once the share is revoked.
   */
  images: z._default(z.array(BlobRef), []),
})
export type SharedBookmark = z.infer<typeof SharedBookmark>

/**
 * Narrows a stored bookmark to what a share is allowed to carry.
 *
 * Images are opt-in rather than the default because only a single-location
 * share can actually serve them — see {@link SharedBookmark.images}.
 */
export function toSharedBookmark(
  b: Bookmark,
  opts: { includeImages?: boolean } = {},
): SharedBookmark {
  return {
    name: bookmarkDisplayName(b),
    coordinates: b.geometry.coordinates,
    address: b.properties.address?.displayText,
    description: b.properties.description,
    links: b.properties.links,
    visited: b.visited,
    favorite: b.favorite,
    zoom: b.zoom,
    images: opts.includeImages ? b.images : [],
  }
}

/**
 * The published payload for a shared list. A CivShare link exposes exactly
 * one document, and the recipient has no access to the publisher's
 * `bookmarks` collection — so the locations are embedded here rather than
 * referenced. Kept in its own synced collection (`listShares`) and rebuilt
 * whenever the underlying list changes, which is what keeps a live share
 * current (cleartext shares are served by reference).
 */
export const SharedList = z.object({
  /** Mirrors the source {@link BookmarkList} id. */
  id: z.string(),
  name: z.string(),
  color: z.optional(z.string()),
  icon: z.optional(z.string()),
  bookmarks: z._default(z.array(SharedBookmark), []),
  createdAt: z.string(),
  /** Latest `updatedAt` across the list and its bookmarks. */
  updatedAt: z.string(),
})
export type SharedList = z.infer<typeof SharedList>

export const LastView = z.object({
  lat: Lat,
  lng: Lng,
  zoom: z.number(),
})
export type LastView = z.infer<typeof LastView>
