import { html, LitElement, nothing, type TemplateResult } from 'lit'
import { getShare } from '@civility/share'
import app, { APP_ID } from '../store/app.ts'
import {
  asZodType,
  type Bookmark,
  type BookmarkList,
  SharedBookmark,
  toSharedBookmark,
} from '../store/schema.ts'
import { validateCoords } from '../utils/coord-validation.ts'
import { icon } from '../utils/icon.ts'
import { linkIconName } from '../utils/link-icon.ts'
import {
  downloadBookmark,
  type ExportFormat,
  renderExportDialog,
} from '../utils/bookmark-export.ts'
import { BOOKMARK_EDIT_EVENT } from '../components/m-bookmark-header.ts'
import { IMAGE_OPEN_EVENT } from '../components/m-lightbox.ts'
import '../components/m-mini-map.ts'

/**
 * How the page is being used, which is a function of who's looking:
 *
 * - `view`   — a public visitor reading a shared link. No local copy exists,
 *              so nothing here can be changed.
 * - `default`— the owner reading their own bookmark. Adds the cheap, common
 *              edits (mark visited, add a photo) inline, without a form.
 * - `edit`   — the owner changing the details, i.e. the old edit dialog.
 */
export type BookmarkMode = 'view' | 'default' | 'edit'

/**
 * The detail page for one location — `#!/bookmark/{id}` for your own, and the
 * rendering target for a shared single-location link.
 *
 * Both sources are read through the public {@link SharedBookmark} projection
 * so the view is written once; the owner modes additionally hold the stored
 * {@link Bookmark} for the fields a share doesn't carry (list membership) and
 * for writes.
 */
export class BookmarkPage extends LitElement {
  static override properties = {
    bookmarkId: { attribute: 'bookmark-id' },
    shareId: { attribute: 'share-id' },
    server: {},
  }

  declare bookmarkId: string
  declare shareId: string
  declare server: string

  #mode: BookmarkMode = 'default'
  #shared: SharedBookmark | null = null
  #error = ''
  #loading = false
  /** Draft rows for the links editor — live only while `#mode === 'edit'`. */
  #editLinks: { label: string; url: string }[] = []
  #exportOpen = false

  protected override createRenderRoot() {
    return this
  }

  override connectedCallback() {
    super.connectedCallback()
    if (this.shareId) {
      this.#mode = 'view'
      this.#loading = true
      void this.#loadShare()
    } else {
      app.addEventListener(this.#onAppUpdate)
      globalThis.addEventListener(BOOKMARK_EDIT_EVENT, this.#onEditRequested)
    }
  }

  override disconnectedCallback() {
    super.disconnectedCallback()
    app.removeEventListener(this.#onAppUpdate)
    globalThis.removeEventListener(BOOKMARK_EDIT_EVENT, this.#onEditRequested)
  }

  // The edit button lives in the header, a separate router landmark.
  #onEditRequested = () => {
    const next = this.#mode === 'edit' ? 'default' : 'edit'
    if (next === 'edit') {
      const b = this.#bookmark()
      this.#editLinks = Object.entries(b?.properties.links ?? {})
        .map(([label, url]) => ({ label, url }))
    }
    this.#setMode(next)
  }

  #addLinkRow = (): void => {
    this.#editLinks = [...this.#editLinks, { label: '', url: '' }]
    this.requestUpdate()
  }

  #removeLinkRow = (index: number): void => {
    this.#editLinks = this.#editLinks.filter((_, i) => i !== index)
    this.requestUpdate()
  }

  #onAppUpdate = () => this.requestUpdate()

  async #loadShare(): Promise<void> {
    try {
      // The server is untrusted; the schema is the trust boundary (§13.6).
      this.#shared = await getShare(APP_ID, this.shareId, {
        server: this.server,
        schema: asZodType(SharedBookmark),
      })
    } catch (e) {
      this.#error = e instanceof Error
        ? e.message
        : 'This shared location is unavailable.'
    } finally {
      this.#loading = false
      this.requestUpdate()
    }
  }

  /** The stored bookmark, when this page is showing one the user owns. */
  #bookmark(): Bookmark | undefined {
    if (this.shareId) return undefined
    return app.bookmarks.find((b) => b.id === this.bookmarkId)
  }

  /** The location in the shape the view renders, whatever its source. */
  #point(): SharedBookmark | null {
    if (this.#shared) return this.#shared
    const b = this.#bookmark()
    return b ? toSharedBookmark(b, { includeImages: true }) : null
  }

  #isOwner(): boolean {
    return !this.shareId && this.#bookmark() !== undefined
  }

  // ====== IMAGES ======

  /** Public blob route for an image on a shared location. */
  #sharedImageUrl(hash: string): string {
    return `${this.server}/share/${encodeURIComponent(APP_ID)}/${
      encodeURIComponent(this.shareId)
    }/blob/${encodeURIComponent(hash)}`
  }

  #imageUrls(): string[] {
    if (this.#shared) {
      return this.#shared.images.map((i) => this.#sharedImageUrl(i.hash))
    }
    // The owner's images ride the service worker's blob proxy: served from
    // the local blob store, or fetched lazily from the sync server with auth.
    return (this.#bookmark()?.images ?? [])
      .map((i) => `/blob/${encodeURIComponent(i.hash)}`)
  }

  async #addImage(e: Event): Promise<void> {
    const input = e.target as HTMLInputElement
    const file = input.files?.[0]
    const b = this.#bookmark()
    if (!file || !b) return
    input.value = ''
    const hash = await app.blobs.put(file, file.type)
    await app.updateBookmark(b.id, {
      images: [...b.images, { hash, mime: file.type, size: file.size }],
    })
  }

  async #removeImage(index: number): Promise<void> {
    const b = this.#bookmark()
    if (!b) return
    await app.updateBookmark(b.id, {
      images: b.images.filter((_, i) => i !== index),
    })
  }

  /** Opens the image in the app-wide lightbox (`m-lightbox`). */
  #openImage(url: string): void {
    this.dispatchEvent(
      new CustomEvent(IMAGE_OPEN_EVENT, {
        bubbles: true,
        composed: true,
        detail: { url },
      }),
    )
  }

  // ====== ACTIONS ======

  async #save(e: Event): Promise<void> {
    e.preventDefault()
    const b = this.#bookmark()
    if (!b) return
    const fd = new FormData(e.target as HTMLFormElement)
    const listIds = fd.getAll('listIds').map(String)
    const addressText = (fd.get('address') as string).trim()
    const [latStr, lngStr] = (fd.get('coords') as string).split(',').map((s) =>
      s.trim()
    )
    const validation = validateCoords(latStr, lngStr)
    if (!validation.ok) {
      if (validation.suggestion) {
        const swap = globalThis.confirm(
          `${validation.error} Swap to ${validation.suggestion.lat}, ${validation.suggestion.lng}?`,
        )
        if (!swap) return
      } else {
        globalThis.alert(validation.error)
        return
      }
    }
    const lat = validation.ok ? Number(latStr) : validation.suggestion!.lat
    const lng = validation.ok ? Number(lngStr) : validation.suggestion!.lng
    const links: Record<string, string> = {}
    for (const { label, url } of this.#editLinks) {
      const l = label.trim()
      const u = url.trim()
      if (l && u) links[l] = u
    }
    await app.updateBookmark(b.id, {
      properties: {
        ...b.properties,
        name: (fd.get('name') as string).trim() || undefined,
        description: (fd.get('description') as string).trim() || undefined,
        // Emptying the field clears the address, matching name/description.
        // The structured geocoder fields go with it — they described the old
        // address, and nothing displays them without a `displayText`.
        address: addressText
          ? { ...b.properties.address, displayText: addressText }
          : undefined,
        links: Object.keys(links).length > 0 ? links : undefined,
      },
      geometry: {
        type: 'Point',
        coordinates: [lng, lat],
      },
      categories: listIds,
    })
    this.#mode = 'default'
    this.requestUpdate()
  }

  async #delete(): Promise<void> {
    const b = this.#bookmark()
    if (!b) return
    if (!globalThis.confirm('Delete this bookmark? This cannot be undone.')) {
      return
    }
    await app.deleteBookmark(b.id)
    globalThis.location.hash = '#!/bookmarks'
  }

  #setMode(mode: BookmarkMode): void {
    this.#mode = mode
    this.requestUpdate()
  }

  // ====== RENDER ======

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

    const point = this.#point()
    if (!point) {
      return html`
        <section class="bookmark-page">
          <p class="share-view-error">
            ${this.#error || 'That bookmark could not be found.'}
          </p>
          <a class="share-view-open" href="#!/bookmarks">Back to bookmarks</a>
        </section>
      `
    }

    const [lng, lat] = point.coordinates
    return html`
      <section class="bookmark-page">
        <!-- Fixed: the detail page is for reading, and "Show on map" opens
            the real map when the visitor wants to explore. -->
        <m-mini-map
          .points="${[point]}"
          .color="${this.#listColor()}"
          .interactive="${false}"
        ></m-mini-map>
        <div class="bookmark-page-body">
          ${this.#mode === 'edit'
            ? this.#renderEdit()
            : this.#renderDetail(point, lat, lng)}
        </div>
      </section>
    `
  }

  /** Every list this bookmark belongs to; a bookmark can be in several. */
  #lists(): BookmarkList[] {
    const ids = this.#bookmark()?.categories ?? []
    return app.bookmarkLists.filter((l) => ids.includes(l.id))
  }

  /** Pin colour follows the first list, since a pin can only be one colour. */
  #listColor(): string | null {
    return this.#lists()[0]?.color ?? null
  }

  /** Which field last showed its "copied" confirmation. */
  #copied = ''

  async #copy(text: string, field: string): Promise<void> {
    try {
      await navigator.clipboard.writeText(text)
      this.#copied = field
      this.requestUpdate()
      setTimeout(() => {
        if (this.#copied !== field) return
        this.#copied = ''
        this.requestUpdate()
      }, 1500)
    } catch {
      // Clipboard access can be denied (insecure context, permissions);
      // the text is on screen and selectable either way.
    }
  }

  #renderCopy(text: string, field: string): TemplateResult {
    const done = this.#copied === field
    return html`
      <button
        class="bookmark-page-copy"
        aria-label="Copy ${field}"
        title="${done ? 'Copied' : `Copy ${field}`}"
        @click="${() => this.#copy(text, field)}"
      >
        ${done ? html`<span aria-hidden="true">✓</span>` : icon('copy', '15px')}
      </button>
    `
  }

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

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

  #doExport = (format: ExportFormat): void => {
    const point = this.#point()
    if (!point) return
    downloadBookmark(point, format)
    this.#dismissExport()
  }

  #renderDetail(
    point: SharedBookmark,
    lat: number,
    lng: number,
  ): TemplateResult {
    const owner = this.#isOwner()
    const images = this.#imageUrls()
    // Only http(s) links are rendered — a shared payload is untrusted, so
    // anything that could carry a javascript:/data: href is dropped.
    const links = Object.entries(point.links ?? {}).filter(([, url]) =>
      /^https?:\/\//i.test(url)
    )
    const lists = owner ? this.#lists() : []
    const coords = `${lat.toFixed(5)}, ${lng.toFixed(5)}`

    return html`
      <div class="bookmark-page-head">
        <div class="bookmark-page-head-text">
          <h1 class="bookmark-page-title">
            ${point.name} ${point.favorite
              ? html`
                <span class="bm-favorite-badge" title="Favorite">${icon(
                  'heart',
                  '1em',
                )}</span>
              `
              : nothing} ${point.visited
              ? html`
                <span class="share-view-visited" title="Visited">${icon(
                  'check-circle',
                  '1em',
                )}</span>
              `
              : nothing}
          </h1>
          ${point.address
            ? html`
              <p class="bookmark-page-field">
                <span class="share-view-address">${point.address}</span>
                ${this.#renderCopy(point.address, 'address')}
              </p>
            `
            : nothing}
          <p class="bookmark-page-field">
            <span class="share-view-coords">${coords}</span>
            ${this.#renderCopy(coords, 'coordinates')}
          </p>
        </div>
      </div>
      ${lists.length
        ? html`
          <p class="bookmark-page-lists">
            ${lists.map((l) =>
              html`
                <a class="bookmark-page-chip" href="#!/list/${l.id}">
                  ${l.color
                    ? html`
                      <span
                        class="bookmark-page-chip-dot"
                        style="background:${l.color}"
                        aria-hidden="true"
                      ></span>
                    `
                    : nothing} ${l.name}
                </a>
              `
            )}
          </p>
        `
        : nothing} ${point.description
        ? html`
          <p class="share-view-description">${point.description}</p>
        `
        : nothing}
      ${images.length || owner ? this.#renderImages(images, owner) : nothing}
      ${links.length
        ? html`
          <ul class="bookmark-page-links">
            ${links.map(([label, url]) =>
              html`
                <li>
                  <a
                    class="bookmark-page-link"
                    href="${url}"
                    target="_blank"
                    rel="noopener noreferrer"
                  >
                    ${icon(linkIconName(url), '16px')}
                    <span>${label}</span>
                  </a>
                </li>
              `
            )}
          </ul>
        `
        : nothing}
      ${owner ? nothing : this.#renderVisitorActions()}
    `
  }

  /**
   * The photo grid. Removal is an edit-mode affordance only — a stray tap on
   * an ✕ shouldn't be able to destroy a photo while you're just looking at
   * the page — but adding stays available in the default view, since it's the
   * common case and is non-destructive.
   */
  #renderImages(images: string[], owner: boolean): TemplateResult {
    const editing = this.#mode === 'edit'
    return html`
      <div class="share-view-images">
        ${images.map((url, i) =>
          html`
            <div class="bm-image-thumb">
              <img
                class="share-view-image"
                src="${url}"
                alt=""
                loading="lazy"
                @click="${() => this.#openImage(url)}"
              >
              ${editing
                ? html`
                  <button
                    type="button"
                    class="bm-image-remove"
                    aria-label="Remove image"
                    @click="${() => this.#removeImage(i)}"
                  >
                    ×
                  </button>
                `
                : nothing}
            </div>
          `
        )} ${owner
          ? html`
            <label class="bm-image-add" title="Add photo">
              +
              <input
                type="file"
                accept="image/*"
                style="display:none"
                @change="${this.#addImage}"
              >
            </label>
          `
          : nothing}
      </div>
    `
  }

  #renderVisitorActions(): TemplateResult {
    const point = this.#point()
    const params = point
      ? new URLSearchParams({
        lat: point.coordinates[1].toFixed(6),
        lng: point.coordinates[0].toFixed(6),
        zoom: String(point.zoom),
        name: point.name,
        ...(point.address ? { address: point.address } : {}),
      })
      : null
    return html`
      <div class="bookmark-page-visitor-actions">
        <a class="share-view-open" href="/?${params}">
          Save to my bookmarks →
        </a>
        <button
          type="button"
          class="bookmark-page-export-btn"
          @click="${this.#openExport}"
        >
          ${icon('download', '15px')} Export
        </button>
      </div>
      ${renderExportDialog(
        this.#exportOpen,
        this.#doExport,
        this.#dismissExport,
      )}
    `
  }

  #renderEdit(): TemplateResult {
    const b = this.#bookmark()
    if (!b) return html``
    const lists = app.bookmarkLists
    return html`
      <form class="bookmark-page-form" @submit="${this.#save}">
        <input
          name="name"
          type="text"
          placeholder="Name"
          .value="${b.properties.name ?? ''}"
          autocomplete="off"
        >
        <textarea
          name="address"
          placeholder="Address"
          rows="2"
          autocomplete="off"
          .value="${b.properties.address?.displayText ?? ''}"
        ></textarea>
        <input
          name="coords"
          type="text"
          placeholder="lng, lat"
          required
          autocomplete="off"
          .value="${b.geometry.coordinates[1]}, ${b.geometry.coordinates[0]}"
        >
        <textarea
          name="description"
          placeholder="Description"
          rows="4"
          autocomplete="off"
          .value="${b.properties.description ?? ''}"
        ></textarea>
        ${lists.length
          ? html`
            <fieldset class="bookmark-page-lists-field">
              <legend>Lists</legend>
              ${lists.map((l) =>
                html`
                  <label class="bookmark-page-list-option">
                    <input
                      type="checkbox"
                      name="listIds"
                      value="${l.id}"
                      .checked="${b.categories.includes(l.id)}"
                    >
                    <span>${l.name}</span>
                  </label>
                `
              )}
            </fieldset>
          `
          : nothing}
        <fieldset class="bookmark-page-links-field">
          <legend>Links</legend>
          ${this.#editLinks.map((row, i) =>
            html`
              <div class="bookmark-page-link-row">
                ${icon(linkIconName(row.url), '16px')}
                <input
                  type="text"
                  placeholder="Label"
                  autocomplete="off"
                  .value="${row.label}"
                  @input="${(e: Event) => {
                    row.label = (e.target as HTMLInputElement).value
                  }}"
                >
                <input
                  type="url"
                  placeholder="https://…"
                  autocomplete="off"
                  .value="${row.url}"
                  @input="${(e: Event) => {
                    row.url = (e.target as HTMLInputElement).value
                    this.requestUpdate()
                  }}"
                >
                <button
                  type="button"
                  class="bookmark-page-link-remove"
                  aria-label="Remove link"
                  @click="${() => this.#removeLinkRow(i)}"
                >
                  ${icon('x', '14px')}
                </button>
              </div>
            `
          )}
          <button
            type="button"
            class="action action--compact"
            @click="${this.#addLinkRow}"
          >
            + Add link
          </button>
        </fieldset>
        ${this.#renderImages(this.#imageUrls(), true)}
        <div class="bm-form-actions">
          <button type="submit">Save</button>
          <button type="button" @click="${() => this.#setMode('default')}">
            Cancel
          </button>
          <button type="button" class="bm-btn-danger" @click="${this.#delete}">
            Delete
          </button>
        </div>
      </form>
    `
  }
}

customElements.define('r-bookmark', BookmarkPage)
