import { html, LitElement, nothing, type TemplateResult } from 'lit'
import { z } from '@zod/zod/mini'
import { getShare } from '@civility/share'
import { APP_ID } from '../store/app.ts'
import { asZodType, SharedBookmark, SharedList } from '../store/schema.ts'
import { icon } from '../utils/icon.ts'
import { ListDetailController } from '../utils/list-detail-view.ts'
import {
  downloadBookmarks,
  type ExportFormat,
  type ExportScopeOption,
  renderExportDialog,
} from '../utils/bookmark-export.ts'
import '../components/m-mini-map.ts'
import './bookmark.ts'

/**
 * Entry point for a public share link (`?id=…&server=…`), rendered by
 * `index.ts`.
 *
 * A share is either one location or a whole list. Single locations are handed
 * to `r-bookmark` in view-only mode — the same page the owner sees, minus the
 * edit affordances — so there's one detail view rather than two that drift.
 * Lists are rendered here, since there's no owner-side equivalent.
 *
 * Payloads are fetched from an untrusted server and validated against our own
 * schema before anything is rendered (§13.6); text goes through Lit, which
 * escapes it.
 */
export class ShareView extends LitElement {
  static override properties = {
    shareId: { attribute: 'share-id' },
    server: {},
    lat: {},
    lng: {},
    zoom: {},
    transit: {},
  }

  declare shareId: string
  declare server: string
  /**
   * Optional `lat`/`lng`/`zoom`/`transit` attributes — present when the list
   * was published with "current view" ({@link app.publishList}'s
   * `useCurrentView`), so the mini-map opens on the sharer's own framing
   * instead of zoomed out to fit every location. Ignored for a single
   * location, which already opens framed on itself.
   */
  declare lat: string
  declare lng: string
  declare zoom: string
  declare transit: string
  #list: SharedList | null = null
  #isList = false
  #error = ''
  #loading = true
  #detail = new ListDetailController(this)

  #exportOpen = false
  #exportScope: 'all' | 'visible' = 'all'
  #importOpen = false
  #importScope: 'all' | 'visible' = 'all'

  protected override createRenderRoot() {
    return this
  }

  override connectedCallback() {
    super.connectedCallback()
    void this.#load()
  }

  /**
   * A share is one of two kinds and the URL doesn't say which, so the payload
   * validates against the union and `bookmarks` (only a list has it) tells
   * them apart. Single locations aren't rendered from this fetch —
   * `r-bookmark` refetches and validates them itself, which keeps that page's
   * shared mode self-contained.
   */
  async #load(): Promise<void> {
    try {
      const data = await getShare(APP_ID, this.shareId, {
        server: this.server,
        schema: asZodType(z.union([SharedList, SharedBookmark])),
      })
      if ('bookmarks' in data) {
        this.#isList = true
        this.#list = data
      }
    } catch (e) {
      this.#error = e instanceof Error
        ? e.message
        : 'This shared location is unavailable.'
    } finally {
      this.#loading = false
      this.requestUpdate()
    }
  }

  /** Validated `lat`/`lng`/`zoom` attributes, or `null` when absent/malformed
   * — coordinates may have been hand-edited or passed around. */
  #initialViewport(): { lat: number; lng: number; zoom: number } | null {
    if (!this.lat || !this.lng) return null
    const lat = Number(this.lat)
    const lng = Number(this.lng)
    if (!Number.isFinite(lat) || lat < -90 || lat > 90) return null
    if (!Number.isFinite(lng) || lng < -180 || lng > 180) return null
    const zoom = Number(this.zoom)
    return {
      lat,
      lng,
      zoom: Number.isFinite(zoom) && zoom > 0 && zoom <= 22 ? zoom : 12,
    }
  }

  /** Every bookmark until the map reports its first viewport, then only
   * those within it. */
  #visibleBookmarks(list: SharedList): SharedBookmark[] {
    return this.#detail.visibleBookmarks(list.bookmarks)
  }

  #openExport = (): void => {
    this.#exportOpen = true
    this.requestUpdate()
  }

  #dismissExport = (): void => {
    this.#exportOpen = false
    this.requestUpdate()
  }

  #setExportScope = (scope: 'all' | 'visible'): void => {
    this.#exportScope = scope
    this.requestUpdate()
  }

  /**
   * Re-derives the current filter rather than trusting `#exportScope` blindly
   * — if the viewport filter has since cleared (the visitor panned back out
   * while the dialog was open), "visible" no longer means anything, so this
   * falls back to exporting everyone instead of an empty or stale scope.
   */
  #doExport = (format: ExportFormat): void => {
    const list = this.#list
    if (!list) return
    const visible = this.#visibleBookmarks(list)
    const filtered = visible.length < list.bookmarks.length
    const points = filtered && this.#exportScope === 'visible'
      ? visible
      : list.bookmarks
    downloadBookmarks(points, list.name, format)
    this.#dismissExport()
  }

  #openImport = (): void => {
    this.#importOpen = true
    this.requestUpdate()
  }

  #dismissImport = (): void => {
    this.#importOpen = false
    this.requestUpdate()
  }

  #setImportScope = (scope: 'all' | 'visible'): void => {
    this.#importScope = scope
    this.requestUpdate()
  }

  /**
   * Hands off to `index.ts`, which re-fetches and re-validates the share
   * rather than trusting this page's in-memory copy — this page never boots
   * the app store, and the payload came from an untrusted server either way.
   * `bounds` rides along only when the visitor chose "visible now" over
   * "all", since a fresh page load has no other way to know what was on
   * screen here.
   */
  #doImport = (): void => {
    const list = this.#list
    if (!list) return
    const visible = this.#visibleBookmarks(list)
    const filtered = visible.length < list.bookmarks.length
    const params = new URLSearchParams({
      import: this.shareId,
      server: this.server,
    })
    if (filtered && this.#importScope === 'visible' && this.#detail.bounds) {
      params.set('bounds', this.#detail.bounds.join(','))
    }
    globalThis.location.href = `/?${params}`
  }

  override render(): TemplateResult {
    if (this.#loading) {
      return html`
        <div class="share-view-loading">
          <ui-spinner size="lg"></ui-spinner>
        </div>
      `
    }

    if (this.#error) {
      return html`
        <section class="share-view">
          <p class="share-view-error">${this.#error}</p>
          <a class="share-view-open" href="/">Open MapsApp →</a>
        </section>
      `
    }

    if (!this.#isList) {
      return html`
        <r-bookmark
          share-id="${this.shareId}"
          server="${this.server}"
        ></r-bookmark>
      `
    }

    return this.#renderList(this.#list!)
  }

  /**
   * A shared list: the map above, the locations below. Import hands off to
   * the app rather than writing here — `index.ts` re-fetches the share and
   * validates it again on that side.
   */
  #renderList(list: SharedList): TemplateResult {
    const count = list.bookmarks.length
    const visible = this.#visibleBookmarks(list)
    const filtered = visible.length < count
    const exportScopeOption: ExportScopeOption | undefined = filtered
      ? {
        allCount: count,
        visibleCount: visible.length,
        scope: this.#exportScope,
        onScopeChange: this.#setExportScope,
      }
      : undefined

    return html`
      <section class="share-view">
        <m-mini-map
          .points="${list.bookmarks}"
          .color="${list.color ?? null}"
          .initialViewport="${this.#initialViewport()}"
          .transitEnabled="${this.transit !== '0'}"
          @mini-map-viewport="${this.#detail.onViewport}"
        ></m-mini-map>
        <div class="share-view-body">
          <div class="share-view-header">
            <h1 class="share-view-title">
              ${list.color
                ? html`
                  <span
                    class="share-view-swatch"
                    style="background:${list.color}"
                    aria-hidden="true"
                  ></span>
                `
                : nothing} ${list.name}
            </h1>
            ${count
              ? html`
                <div class="share-view-actions">
                  <button
                    type="button"
                    class="share-view-action-btn"
                    @click="${this.#openImport}"
                  >
                    ${icon('bookmark', '16px')}
                    <span class="share-view-action-label">
                      Add to Bookmarks
                    </span>
                  </button>
                  <button
                    type="button"
                    class="share-view-action-btn"
                    @click="${this.#openExport}"
                  >
                    ${icon('download', '16px')}
                    <span class="share-view-action-label">Export</span>
                  </button>
                </div>
              `
              : nothing}
          </div>
          <p class="share-view-coords">
            ${filtered
              ? html`${visible.length} of ${count} locations`
              : html`${count} ${count === 1 ? 'location' : 'locations'}`}
            ${filtered
              ? html`
                <button
                  type="button"
                  class="link share-view-reset"
                  @click="${() => this.#detail.resetView()}"
                >
                  Show all
                </button>
              `
              : nothing}
          </p>
          <ol class="share-view-list">
            ${visible.length
              ? visible.map((b) => this.#detail.renderListItem(b))
              : filtered
              ? html`
                <li class="share-view-list-empty">No locations in view.</li>
              `
              : nothing}
          </ol>
        </div>
      </section>
      ${renderExportDialog(
        this.#exportOpen,
        this.#doExport,
        this.#dismissExport,
        exportScopeOption,
      )}
      ${this.#renderImportDialog(count, visible.length, filtered)}
    `
  }

  #renderImportDialog(
    count: number,
    visibleCount: number,
    filtered: boolean,
  ): TemplateResult {
    return html`
      <ui-dialog ?open="${this.#importOpen}" @dismiss="${this.#dismissImport}">
        <dialog>
          <article class="bm-dialog">
            <h2 class="bm-dialog-title">Add to Bookmarks</h2>
            <p>
              Save ${filtered ? 'these locations' : 'this list'} to your own
              bookmarks.
            </p>
            ${filtered
              ? html`
                <div class="bm-scope-toggle">
                  <button
                    type="button"
                    class="bm-scope-btn"
                    aria-pressed="${this.#importScope === 'all'}"
                    @click="${() => this.#setImportScope('all')}"
                  >
                    All ${count}
                  </button>
                  <button
                    type="button"
                    class="bm-scope-btn"
                    aria-pressed="${this.#importScope === 'visible'}"
                    @click="${() => this.#setImportScope('visible')}"
                  >
                    Visible now (${visibleCount})
                  </button>
                </div>
              `
              : nothing}
            <div class="bm-form-actions">
              <button
                type="button"
                class="action action--compact"
                @click="${this.#doImport}"
              >
                Add to Bookmarks
              </button>
              <button
                type="button"
                class="link"
                @click="${this.#dismissImport}"
              >
                Cancel
              </button>
            </div>
          </article>
        </dialog>
      </ui-dialog>
    `
  }
}

customElements.define('r-share-view', ShareView)
