import type {Crumb, PlaceRef} from '@crumbs/core/schemas'
import {useEffect, useState} from 'react'
import {InteractionManager} from 'react-native'

import type {
  CrumbFormFields,
  CrumbImageAction,
} from '@/components/composer'
import {
  crumbToDraft,
  hasCanonicalPlace,
  placeToDraft,
  type Draft,
} from '@/lib/draft'
import {metersBetween, reverseGeocode} from '@/lib/geocode'
import {suggestTrailFor} from '@/lib/suggest-trail'
import {
  useAddCrumb,
  useCrumb,
  useCrumbDetailLoading,
  useLoadCrumbDetail,
  useRemoveCrumbImage,
  useUpdateCrumb,
  useUploadCrumbImage,
} from '@/stores/crumbs'
import {useRefreshTags} from '@/stores/tags'

const PLACE_MATCH_M = 60
const ADDRESS_MATCH_M = 250

let pendingDraft: Draft | null = null

export const setPendingPlace = (ref: PlaceRef) => {
  pendingDraft = placeToDraft(ref)
}
export const setPendingDraft = (draft: Draft) => {
  pendingDraft = draft
}
export const clearPendingDraft = () => {
  pendingDraft = null
}

export const useEditableDraft = (crumbId: string | null) => {
  const crumb = useCrumb(crumbId ?? '')
  const loadDetail = useLoadCrumbDetail()
  const detailLoading = useCrumbDetailLoading(crumbId ?? '')
  const addCrumb = useAddCrumb()
  const updateCrumb = useUpdateCrumb()
  const uploadImage = useUploadCrumbImage()
  const removeImage = useRemoveCrumbImage()
  const refreshTags = useRefreshTags()

  const applyImage = async (id: string, image: CrumbImageAction) => {
    try {
      if (image.action === 'set') await uploadImage(id, image.uri)
      else if (image.action === 'remove') await removeImage(id)
    } catch (err) {
      // the crumb is already saved and the upload is queued; a failure to
      // stage its image is a rare non-fatal file-system error, not worth
      // surfacing over a saved crumb
      console.warn('crumb image apply failed', err)
    }
  }

  // editing may target a crumb beyond the loaded feed page; pull it into the
  // detail cache so the form can hydrate
  useEffect(() => {
    if (crumbId) void loadDetail(crumbId)
  }, [crumbId, loadDetail])

  // capture the ferried draft once at mount (compose mode)
  const [pending, setPending] = useState(() =>
    crumbId ? null : pendingDraft,
  )
  const [inferring, setInferring] = useState(
    () =>
      !crumbId &&
      !!pending &&
      !pending.placeRef &&
      !pending.placeId &&
      !pending.address &&
      pending.latitude != null &&
      pending.longitude != null,
  )

  // infer address (and place link when close) from a raw map-press draft;
  // wait for the sheet's present animation to finish before doing async
  // work, otherwise the in-flight work dismisses the formSheet
  useEffect(() => {
    if (
      !inferring ||
      !pending ||
      pending.latitude == null ||
      pending.longitude == null
    ) {
      return
    }
    const origin = {
      latitude: pending.latitude,
      longitude: pending.longitude,
    }
    let active = true
    const task = InteractionManager.runAfterInteractions(() => {
      void reverseGeocode(origin)
        .then((ref) => {
          if (!active || !ref) return
          const dist = metersBetween(origin, ref.coordinate)
          if (dist > ADDRESS_MATCH_M) return
          const linkPlace = dist <= PLACE_MATCH_M
          setPending((p) =>
            p
              ? {
                  ...p,
                  address: p.address ?? ref.address ?? null,
                  ...(linkPlace && {
                    name: p.name || ref.name,
                    category: p.category ?? ref.category ?? null,
                    placeRef: p.placeRef ?? ref,
                  }),
                }
              : p,
          )
        })
        // best-effort inference; failure just leaves the fields blank
        .catch(() => {})
        .finally(() => {
          if (active) setInferring(false)
        })
    })
    return () => {
      active = false
      task.cancel()
    }
    // run once on mount for compose; pending identity is stable
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  const draft = crumbId
    ? crumb
      ? crumbToDraft(crumb)
      : null
    : pending
  const resolving = crumbId != null && !crumb && detailLoading

  // returns the created crumb (compose) so the caller can open it; undefined
  // when editing
  const submit = async ({
    name,
    note,
    address,
    tags,
    image,
  }: CrumbFormFields): Promise<Crumb | undefined> => {
    if (crumbId && crumb) {
      // an address edit on a non-canonical location re-picks the place
      // as a fresh provisional; canonical place data is never mutated
      const addressChanged =
        !hasCanonicalPlace(crumbToDraft(crumb)) &&
        (address ?? null) !== (crumb.place?.address ?? null)
      await updateCrumb(crumb.id, {
        name,
        note,
        tags,
        ...(addressChanged &&
          address && {placeDraft: {name, address}}),
      })
      await applyImage(crumb.id, image)
      void refreshTags()
      return undefined
    }
    if (pending) {
      const created = await addCrumb({
        draft: {...pending, name, note, address},
        tags,
      })
      void suggestTrailFor(created.id)
      void refreshTags()
      // the image is queued in the crumb's own scope, so it replays after the
      // create lands (fifo) — no need to wait on the create settling first
      if (image.action !== 'keep') void applyImage(created.id, image)
      return created
    }
    void refreshTags()
    return undefined
  }

  return {draft, submit, inferring, resolving}
}
