import { ref, watch, nextTick, type Ref } from 'vue'
import { useRoute, useRouter } from '#imports'

interface ParamDef<T extends string | number = string | number> {
  default: T
  type?: 'string' | 'number'
  /** Debounce URL writes (ms). Default: 0 (immediate). */
  debounce?: number
}

type ParamDefs = Record<string, ParamDef>

type InferRef<D extends ParamDef> = D['default'] extends number ? Ref<number> : Ref<string>

type StateRefs<T extends ParamDefs> = {
  [K in keyof T]: InferRef<T[K]>
}

interface UseUrlStateOptions {
  /** Called when state is restored from URL (back/forward navigation). */
  onRestore?: () => void
}

export function useUrlState<T extends ParamDefs>(
  params: T,
  options?: UseUrlStateOptions,
): StateRefs<T> {
  const route = useRoute()
  const router = useRouter()

  const keys = Object.keys(params) as (keyof T & string)[]
  const refs: Record<string, Ref<string | number>> = {}
  const timers: Record<string, ReturnType<typeof setTimeout> | null> = {}

  for (const key of keys) {
    refs[key] = ref(parseValue(route.query[key], params[key]!))
    timers[key] = null
  }

  let isProgrammatic = false

  const buildQuery = (): Record<string, string> => {
    const q: Record<string, string> = {}
    for (const key of keys) {
      const val = refs[key]!.value
      if (val !== params[key]!.default) {
        q[key] = String(val)
      }
    }
    return q
  }

  const pushToUrl = () => {
    if (isProgrammatic) return
    isProgrammatic = true
    const query = buildQuery()
    router
      .replace({ path: route.path, query })
      .catch(() => {})
      .finally(() => {
        nextTick(() => {
          isProgrammatic = false
        })
      })
  }

  for (const key of keys) {
    const ms = params[key]!.debounce ?? 0
    watch(refs[key]!, () => {
      if (isProgrammatic) return
      if (ms > 0) {
        if (timers[key]) clearTimeout(timers[key]!)
        timers[key] = setTimeout(pushToUrl, ms)
      } else {
        pushToUrl()
      }
    })
  }

  watch(
    () => ({ ...route.query }),
    (newQuery) => {
      if (isProgrammatic) return
      isProgrammatic = true
      let changed = false
      for (const key of keys) {
        const parsed = parseValue(newQuery[key], params[key]!)
        if (refs[key]!.value !== parsed) {
          refs[key]!.value = parsed
          changed = true
        }
      }
      nextTick(() => {
        isProgrammatic = false
        if (changed && options?.onRestore) {
          options.onRestore()
        }
      })
    },
  )

  return refs as StateRefs<T>
}

function parseValue(
  raw: unknown,
  def: ParamDef,
): string | number {
  const str = typeof raw === 'string' ? raw : null
  if (str === null) return def.default
  if (def.type === 'number' || typeof def.default === 'number') {
    const n = Number(str)
    return Number.isFinite(n) ? n : def.default
  }
  return str
}
