import { html, LitElement, type TemplateResult } from 'lit'
import app from '../store/app.ts'
import { type Bookmark, bookmarkDisplayName } from '../store/schema.ts'
import { from as nominatimToProperties } from '../store/adapters/nominatim.ts'
import { setMapNav } from '../utils/nav.ts'
import { coordsFromDirectInput } from '../utils/geocode.ts'
import { type NominatimResult, nominatimSearch } from '../utils/nominatim.ts'
import { icon } from '../utils/icon.ts'

function zoomFromBoundingbox(bb: [string, string, string, string]): number {
  const latSpan = Math.abs(parseFloat(bb[1]) - parseFloat(bb[0]))
  const lngSpan = Math.abs(parseFloat(bb[3]) - parseFloat(bb[2]))
  const span = Math.max(latSpan, lngSpan)
  if (span < 0.01) return 15
  if (span < 0.05) return 13
  if (span < 0.2) return 11
  if (span < 1) return 9
  if (span < 5) return 7
  if (span < 20) return 5
  return 3
}

export class SearchPage extends LitElement {
  private history = app.searchHistory
  private inputValue = ''
  private bookmarkResults: Bookmark[] = []
  private results: NominatimResult[] = []
  private loading = false
  private error: string | null = null
  private lastQuery = ''
  /**
   * Identifies the latest search; bumped on every search and on typing.
   * A resolved request that no longer matches is stale — a slower, older
   * response must not clobber newer results or a cleared input.
   */
  #searchSeq = 0

  protected override createRenderRoot() {
    return this
  }

  override connectedCallback() {
    super.connectedCallback()
    this.history = app.searchHistory
    app.addEventListener(this.#onAppUpdate)
  }

  override disconnectedCallback() {
    super.disconnectedCallback()
    app.removeEventListener(this.#onAppUpdate)
  }

  #onAppUpdate = () => {
    this.history = app.searchHistory
    this.requestUpdate()
  }

  #search = async (query: string) => {
    const direct = await coordsFromDirectInput(query)
    if (direct) {
      setMapNav({ ...direct, marker: true, name: query })
      location.hash = '#!/'
      return
    }

    this.lastQuery = query
    this.bookmarkResults = this.#matchBookmarks(query)

    if (!app.onlineSearchEnabled) {
      this.loading = false
      this.error = null
      this.results = []
      this.requestUpdate()
      return
    }

    this.loading = true
    this.error = null
    this.results = []
    this.requestUpdate()
    const seq = ++this.#searchSeq
    try {
      const results = await nominatimSearch(query)
      if (seq !== this.#searchSeq) return
      this.results = results
      await app.addSearch(query)
    } catch {
      if (seq !== this.#searchSeq) return
      this.error = 'Search failed. Please try again.'
    } finally {
      if (seq === this.#searchSeq) {
        this.loading = false
        this.requestUpdate()
      }
    }
  }

  #matchBookmarks(query: string): Bookmark[] {
    const q = query.toLowerCase()
    return app.bookmarks.filter((b) => {
      const name = bookmarkDisplayName(b).toLowerCase()
      return (
        name.includes(q) ||
        b.properties.address?.displayText?.toLowerCase().includes(q) ||
        b.properties.displayName?.toLowerCase().includes(q)
      )
    })
  }

  #handleSearch = async (e: Event) => {
    e.preventDefault()
    const form = e.target as HTMLFormElement
    const input = form.querySelector<HTMLInputElement>('input[type="search"]')
    const query = input?.value.trim()
    if (!query) return
    await this.#search(query)
  }

  #handleBookmarkClick = (bookmark: Bookmark) => {
    const [lng, lat] = bookmark.geometry.coordinates
    setMapNav({ lat, lng, zoom: bookmark.zoom, bookmarkId: bookmark.id })
    location.hash = '#!/'
  }

  #handleResultClick = (result: NominatimResult) => {
    const shortName = result.display_name.split(',')[0].trim()
    setMapNav({
      lat: parseFloat(result.lat),
      lng: parseFloat(result.lon),
      zoom: zoomFromBoundingbox(result.boundingbox),
      marker: true,
      name: shortName,
      address: result.display_name,
    })
    location.hash = '#!/'
  }

  #handleSaveResult = async (result: NominatimResult) => {
    const lat = parseFloat(result.lat)
    const lng = parseFloat(result.lon)
    const zoom = zoomFromBoundingbox(result.boundingbox)
    await app.addBookmark(lat, lng, zoom, nominatimToProperties(result))
    // Promote it: drop the online result and re-run the local match so the
    // row reappears above as one of the user's own bookmarks.
    this.results = this.results.filter((r) => r.place_id !== result.place_id)
    this.bookmarkResults = this.#matchBookmarks(this.lastQuery)
    this.requestUpdate()
  }

  #handleInput = (e: Event) => {
    this.#searchSeq++ // typing invalidates any search still in flight
    this.inputValue = (e.target as HTMLInputElement).value
    this.bookmarkResults = []
    this.results = []
    this.error = null
    this.loading = false
    this.requestUpdate()
  }

  #handleHistoryClick = async (query: string) => {
    this.inputValue = query
    const input = this.querySelector<HTMLInputElement>('input[type="search"]')
    if (input) input.value = query
    await this.#search(query)
  }

  #handleClearHistory = async () => {
    await app.clearHistory()
  }

  override render(): TemplateResult {
    return html`
      <form class="search-input-wrap" @submit="${this.#handleSearch}">
        <input
          type="search"
          placeholder="Search for a place..."
          autocomplete="off"
          autocorrect="off"
          spellcheck="false"
          @input="${this.#handleInput}"
        >
      </form>

      ${(() => {
        const q = this.inputValue.trim().toLowerCase()
        const predict = q && !this.loading && !this.error &&
          this.bookmarkResults.length === 0 && this.results.length === 0
        const predBookmarks = predict ? this.#matchBookmarks(q) : []
        const predHistory = predict
          ? this.history.filter((e) => e.query.toLowerCase().includes(q))
          : []

        if (this.loading) {
          return html`
            ${this.bookmarkResults.length > 0
              ? html`
                <div class="search-history-list" role="list">
                  ${this.bookmarkResults.map((b) =>
                    this.#renderBookmarkResult(b)
                  )}
                </div>
              `
              : html`
              `}
            <p class="search-empty">Searching…</p>
          `
        }

        if (this.error) {
          return html`
            <p class="search-empty">${this.error}</p>
          `
        }

        if (this.bookmarkResults.length > 0 || this.results.length > 0) {
          return html`
            <div class="search-history-list" role="list">
              ${this.bookmarkResults.map((b) =>
                this.#renderBookmarkResult(b)
              )} ${this.results.map((result) =>
                this.#renderNominatimResult(result)
              )}
            </div>
          `
        }

        if (predBookmarks.length > 0 || predHistory.length > 0) {
          return html`
            ${predBookmarks.length > 0
              ? html`
                <p class="search-section-title">Bookmarks</p>
                <div class="search-history-list" role="list">
                  ${predBookmarks.map((b) => this.#renderBookmarkResult(b))}
                </div>
              `
              : html`
              `} ${predHistory.length > 0
              ? html`
                <p class="search-section-title">Recent Searches</p>
                <div class="search-history-list" role="list">
                  ${predHistory.map((entry) =>
                    html`
                      <button
                        class="search-history-item"
                        role="listitem"
                        @click="${() => this.#handleHistoryClick(entry.query)}"
                      >
                        <span>${entry.query}</span>
                        ${icon('navigation', '16px')}
                      </button>
                    `
                  )}
                </div>
              `
              : html`
              `}
          `
        }

        if (this.history.length > 0) {
          return html`
            <div class="search-history-clear">
              <button @click="${this
                .#handleClearHistory}">Clear history</button>
            </div>
            <div class="search-history-list" role="list">
              ${this.history.map((entry) =>
                html`
                  <button
                    class="search-history-item"
                    role="listitem"
                    @click="${() => this.#handleHistoryClick(entry.query)}"
                  >
                    <span>${entry.query}</span>
                    ${icon('navigation', '16px')}
                  </button>
                `
              )}
            </div>
          `
        }

        return html`
          <p class="search-empty">Search for a location to get started.</p>
        `
      })()}
    `
  }

  #renderBookmarkResult(b: Bookmark): TemplateResult {
    return html`
      <button
        class="search-history-item"
        role="listitem"
        @click="${() => this.#handleBookmarkClick(b)}"
      >
        <span>${bookmarkDisplayName(b)}</span>
        ${icon('bookmark', '16px')}
      </button>
    `
  }

  #renderNominatimResult(result: NominatimResult): TemplateResult {
    return html`
      <div class="search-history-item search-result-row" role="listitem">
        <button
          class="search-result-name"
          @click="${() => this.#handleResultClick(result)}"
        >
          <span>${result.display_name}</span>
        </button>
        <button
          class="search-result-save"
          aria-label="Save bookmark"
          @click="${() => this.#handleSaveResult(result)}"
        >
          ${icon('bookmark', '16px')}
        </button>
      </div>
    `
  }
}

customElements.define('r-search', SearchPage)
