import { html, LitElement, type TemplateResult } from 'lit'
import {
  deletePMTiles,
  downloadAndSavePMTiles,
  isPMTilesCached,
} from '../utils/fs.ts'
import {
  fetchTileManifest,
  slugToLabel,
  type TileManifestEntry,
} from '../utils/tile-manifest.ts'
import { icon } from '../utils/icon.ts'

type TileStatus =
  | 'checking'
  | 'cached'
  | 'available'
  | 'downloading'
  | 'deleting'
  | 'error'

type TreeNode = {
  tiles: TileManifestEntry[]
  children: Map<string, TreeNode>
}

export class SettingsDownloadsPage extends LitElement {
  private tiles: TileManifestEntry[] = []
  private statuses: Record<string, TileStatus> = {}
  private errors: Record<string, string> = {}
  private filter = ''

  protected override createRenderRoot() {
    return this
  }

  override async connectedCallback() {
    super.connectedCallback()
    this.tiles = await fetchTileManifest()
    this.statuses = Object.fromEntries(
      this.tiles.map((t) => [t.id, 'checking']),
    )
    this.requestUpdate()
    await this.#checkStatuses()
  }

  async #checkStatuses(): Promise<void> {
    await Promise.all(
      this.tiles.map(async (tile) => {
        if (!tile.filename) return
        const cached = await isPMTilesCached(tile.filename)
        this.statuses[tile.id] = cached ? 'cached' : 'available'
      }),
    )
    this.requestUpdate()
  }

  async #handleDownload(tile: TileManifestEntry): Promise<void> {
    if (!tile.filename) return
    this.statuses[tile.id] = 'downloading'
    this.requestUpdate()
    try {
      await downloadAndSavePMTiles(tile.path!, tile.filename)
      this.statuses[tile.id] = 'cached'
      globalThis.dispatchEvent(new CustomEvent('pmtiles-updated'))
    } catch (err) {
      this.statuses[tile.id] = 'error'
      this.errors[tile.id] = err instanceof Error
        ? err.message
        : 'Download failed'
    }
    this.requestUpdate()
  }

  async #handleDelete(tile: TileManifestEntry): Promise<void> {
    if (!tile.filename) return
    this.statuses[tile.id] = 'deleting'
    this.requestUpdate()
    try {
      await deletePMTiles(tile.filename)
      this.statuses[tile.id] = 'available'
    } catch (err) {
      this.statuses[tile.id] = 'error'
      this.errors[tile.id] = err instanceof Error
        ? err.message
        : 'Delete failed'
    }
    this.requestUpdate()
  }

  #buildTree(tiles: TileManifestEntry[]): Map<string, TreeNode> {
    const root = new Map<string, TreeNode>()

    const tileEntries = tiles.filter((t) => t.filename)

    for (const tile of tileEntries) {
      const segments = tile.group ? tile.group.split('/') : ['other']
      this.#insertTile(root, segments, tile)
    }

    return root
  }

  #insertTile(
    map: Map<string, TreeNode>,
    segments: string[],
    tile: TileManifestEntry,
  ): void {
    const [head, ...rest] = segments
    if (!map.has(head)) {
      map.set(head, { tiles: [], children: new Map() })
    }
    const node = map.get(head)!
    if (rest.length === 0) {
      node.tiles.push(tile)
    } else {
      this.#insertTile(node.children, rest, tile)
    }
  }

  #renderTree(map: Map<string, TreeNode>): TemplateResult {
    const sorted = [...map.entries()].sort(([a], [b]) => {
      if (a === 'world') return -1
      if (b === 'world') return 1
      return a.localeCompare(b)
    })
    return html`
      ${sorted.map(([key, node]) => {
        const directTiles = [...node.tiles].sort((a, b) =>
          a.label.localeCompare(b.label)
        )
        return html`
          <details>
            <summary>${slugToLabel(key)}</summary>
            ${node.children.size > 0
              ? this.#renderTree(node.children)
              : ''} ${directTiles.length > 0
              ? html`
                <div class="tile-list">
                  ${directTiles.map((tile) => this.#renderTileItem(tile))}
                </div>
              `
              : ''}
          </details>
        `
      })}
    `
  }

  override render(): TemplateResult {
    const q = this.filter.toLowerCase()
    const filteredTiles = q
      ? this.tiles.filter((t) =>
        t.filename &&
        (t.label.toLowerCase().includes(q) ||
          t.group.toLowerCase().includes(q))
      )
      : null

    return html`
      <section>
        <h2>Offline Maps</h2>
        <p>
          Download regions to use the app without an internet connection. Downloaded
          maps are stored in your browser and persist across sessions.
        </p>
        <input
          class="w-100"
          type="search"
          placeholder="Search regions…"
          .value="${this.filter}"
          @input="${(e: Event) => {
            this.filter = (e.target as HTMLInputElement).value
            this.requestUpdate()
          }}"
        >
        ${filteredTiles
          ? html`
            <div class="tile-list">
              ${filteredTiles.map((tile) => this.#renderTileItem(tile))}
            </div>
          `
          : this.#renderTree(this.#buildTree(this.tiles))}
      </section>
    `
  }

  #renderTileItem(tile: TileManifestEntry): TemplateResult {
    const status = this.statuses[tile.id] ?? 'available'
    return html`
      <div class="tile-item">
        <div class="tile-item-info">
          <span class="tile-item-name">${tile.label}</span>
          ${tile.description
            ? html`
              <div class="tile-item-meta">${tile.description}</div>
            `
            : ''} ${status === 'error'
            ? html`
              <div class="tile-item-meta settings-data-status--error">
                ${this.errors[tile.id] ?? 'Download failed'}
              </div>
            `
            : ''}
        </div>
        <div class="tile-item-action">
          ${this.#renderTileAction(tile, status)}
        </div>
      </div>
    `
  }

  #renderTileAction(
    tile: TileManifestEntry,
    status: TileStatus,
  ): TemplateResult {
    if (status === 'checking') {
      return html`
        <ui-spinner></ui-spinner>
      `
    }
    if (status === 'cached') {
      return html`
        <span class="tile-status-cached">✓ Downloaded</span>
        <button
          class="action action--compact"
          @click="${() => this.#handleDelete(tile)}"
        >
          ${icon('x', '16px')}
          Remove
        </button>
      `
    }
    if (status === 'downloading' || status === 'deleting') {
      return html`
        <ui-spinner></ui-spinner>
      `
    }
    return html`
      <button
        class="action action--compact"
        @click="${() => this.#handleDownload(tile)}"
      >
        ${icon('download', '16px')}
        Download
      </button>
    `
  }
}

customElements.define('r-settings-downloads', SettingsDownloadsPage)
