/**
 * public/tabs.ts — multi-tab manager + tab bar UI.
 *
 * Each tab owns an independent TerminalSession (own WS + server session).
 *
 * Titles: auto from the terminal (current folder, via folderFromTitle) →
 * double-click to rename inline → falls back to "Term N". Manual wins.
 * Interaction: pointerdown to switch (robust with draggable), drag to reorder,
 * middle-click / × to close, status dot (connection color), unread dot.
 *
 * DOM is built once per tab and updated in place. A full rebuild happens ONLY
 * on structural changes (add / close / reorder / rename) — never on a plain
 * activate or on background output — so clicks are never eaten by a re-render.
 *
 * Closing a tab only disconnects its WS (a *detach*); the server PTY keeps
 * running and is reclaimed by IDLE_TTL.
 *
 * v0.6 (P6): the home screen has a segmented control ("Sessions" / "Projects")
 * that switches between the session launcher and the projects panel. State is
 * persisted to localStorage. openProject() spawns a new claude session in a
 * repo directory, labelled with the repo name (deduped with #N suffixes).
 */

import { TerminalSession } from './terminal-session.js'
import { THEMES, DEFAULT_SETTINGS, type Settings } from './settings.js'
import { mountLauncher, type Launcher } from './launcher.js'
import { mountProjects, type ProjectsPanel } from './projects.js'

const TABS_KEY = 'web-terminal:tabs'
const ACTIVE_KEY = 'web-terminal:active'
const HOME_VIEW_KEY = 'web-terminal:home-view'

type HomeView = 'sessions' | 'projects'

interface TabEntry {
  session: TerminalSession
  customTitle: string | null // user-set; null = use auto/fallback (persisted)
  autoTitle: string | null // current folder from the terminal title
  hasActivity: boolean // inactive tab got output since last viewed
  el: HTMLDivElement | null // the .tab element (updated in place)
}

interface StoredTab {
  id: string | null
  title: string | null
}

export class TabApp {
  private readonly tabs: TabEntry[] = []
  private activeIndex = -1
  private editingIndex = -1
  private dragIndex = -1
  private notifyAsked = false
  private settings: Settings = DEFAULT_SETTINGS
  private readonly paneHost: HTMLElement
  private readonly tabBar: HTMLElement
  private readonly approvalBar: HTMLDivElement
  private readonly launcher: Launcher
  private readonly projects: ProjectsPanel
  private homeView: HomeView = 'sessions'
  private readonly segControl: HTMLElement

  constructor(paneHost: HTMLElement, tabBar: HTMLElement) {
    this.paneHost = paneHost
    this.tabBar = tabBar
    this.approvalBar = document.createElement('div')
    this.approvalBar.id = 'approvalbar'
    this.approvalBar.style.display = 'none'
    document.body.appendChild(this.approvalBar)

    this.launcher = mountLauncher(this.paneHost, {
      onOpen: (id) => this.openSession(id),
      onNew: () => this.newTab(),
    })

    // Load persisted home view choice.
    try {
      const stored = localStorage.getItem(HOME_VIEW_KEY)
      if (stored === 'sessions' || stored === 'projects') this.homeView = stored
    } catch {
      // localStorage unavailable — use default
    }

    this.projects = mountProjects(this.paneHost, {
      onOpenProject: (repoPath, repoName) => this.openProject(repoPath, repoName),
      onEnterSession: (id) => this.openSession(id),
    })

    this.segControl = this.buildSegControl()
    this.paneHost.appendChild(this.segControl)

    // v0.5: do NOT auto-create or auto-restore tabs. Land on the home screen;
    // the user picks which session to open. Sessions persist server-side, so
    // opening one replays its full scrollback.
    this.rebuild()
    this.updateHomeView()
  }

  /* ── Home-view visibility ────────────────────────────────────────── */

  /**
   * Controls visibility of the segmented control and both home sub-panels.
   * When tabs are open: hide everything (the terminal panes own the screen).
   * When no tab is open: show the seg control and the selected sub-panel.
   */
  private updateHomeView(): void {
    const empty = this.tabs.length === 0

    // Sync active-class on segmented control buttons.
    const btns = this.segControl.querySelectorAll<HTMLButtonElement>('.home-seg-btn')
    btns.forEach((btn, i) => {
      const view: HomeView = i === 0 ? 'sessions' : 'projects'
      btn.classList.toggle('active', view === this.homeView)
    })

    if (empty) {
      this.segControl.style.display = 'flex'
      this.launcher.setVisible(this.homeView === 'sessions')
      this.projects.setVisible(this.homeView === 'projects')
    } else {
      this.segControl.style.display = 'none'
      this.launcher.setVisible(false)
      this.projects.setVisible(false)
    }
  }

  private saveHomeView(): void {
    try {
      localStorage.setItem(HOME_VIEW_KEY, this.homeView)
    } catch {
      // localStorage unavailable
    }
  }

  private buildSegControl(): HTMLElement {
    const seg = document.createElement('div')
    seg.className = 'home-seg'
    seg.style.display = 'none' // updateHomeView() will show it when needed

    const sessBtn = document.createElement('button')
    sessBtn.className = 'home-seg-btn'
    sessBtn.textContent = 'Sessions'
    sessBtn.addEventListener('click', () => {
      this.homeView = 'sessions'
      this.saveHomeView()
      this.updateHomeView()
    })

    const projBtn = document.createElement('button')
    projBtn.className = 'home-seg-btn'
    projBtn.textContent = 'Projects'
    projBtn.addEventListener('click', () => {
      this.homeView = 'projects'
      this.saveHomeView()
      this.updateHomeView()
    })

    seg.append(sessBtn, projBtn)
    return seg
  }

  /* ── Approval bar ───────────────────────────────────────────────── */

  /** Show/hide the approve/reject banner for the active tab's held request (H3). */
  private updateApprovalBar(): void {
    const session = this.tabs[this.activeIndex]?.session
    if (!session || !session.pendingApproval) {
      this.approvalBar.style.display = 'none'
      return
    }
    this.approvalBar.replaceChildren()
    const label = document.createElement('span')
    label.className = 'approval-label'
    label.textContent = `Claude wants to use ${session.pendingTool ?? 'a tool'}`
    const approve = document.createElement('button')
    approve.className = 'approval-yes'
    approve.textContent = '✓ Approve'
    approve.addEventListener('click', () => {
      session.approve()
      this.updateApprovalBar()
    })
    const reject = document.createElement('button')
    reject.className = 'approval-no'
    reject.textContent = '✗ Reject'
    reject.addEventListener('click', () => {
      session.reject()
      this.updateApprovalBar()
    })
    this.approvalBar.append(label, approve, reject)
    this.approvalBar.style.display = 'flex'
  }

  private displayTitle(entry: TabEntry, idx: number): string {
    return entry.customTitle ?? entry.autoTitle ?? `Term ${idx + 1}`
  }

  /* ── persistence ─────────────────────────────────────────────────── */

  private persist(): void {
    try {
      const stored: StoredTab[] = this.tabs.map((t) => ({ id: t.session.id, title: t.customTitle }))
      localStorage.setItem(TABS_KEY, JSON.stringify(stored))
      localStorage.setItem(ACTIVE_KEY, String(this.activeIndex))
    } catch {
      // localStorage unavailable — run without persistence
    }
  }

  /** The active tab's server session id (null until it has attached). */
  activeSessionId(): string | null {
    return this.tabs[this.activeIndex]?.session.id ?? null
  }

  /** Focus the tab for `sessionId`, or open one that joins it (share-link target). */
  openSession(sessionId: string): void {
    const existing = this.tabs.findIndex((t) => t.session.id === sessionId)
    if (existing >= 0) {
      this.activate(existing)
      return
    }
    this.addEntry(sessionId, null)
    this.persist()
    this.rebuild()
    this.activate(this.tabs.length - 1)
  }

  /* ── tab lifecycle ───────────────────────────────────────────────── */

  private addEntry(
    sessionId: string | null,
    customTitle: string | null,
    cwd?: string,
    initialInput?: string,
  ): TabEntry {
    // Build the session FIRST so `entry` is never typed with a null session.
    // The callbacks below capture `entry` by reference; they only fire after
    // construction (async), by which point `entry` is assigned.
    let entry: TabEntry
    const session = new TerminalSession({
      sessionId,
      ...(cwd !== undefined ? { cwd } : {}),
      ...(initialInput !== undefined ? { initialInput } : {}),
      onSessionId: () => this.persist(),
      // onActivity only fires for hidden (inactive) panes (see TerminalSession).
      onActivity: () => {
        entry.hasActivity = true
        this.refreshTab(entry)
      },
      onTitle: (title) => {
        entry.autoTitle = title.trim() || null
        this.refreshTab(entry)
      },
      onStatus: () => this.refreshTab(entry),
      onClaudeStatus: (status) => {
        this.refreshTab(entry)
        this.updateApprovalBar()
        // Notify when a background tab needs approval (H2/H4).
        if (status === 'waiting' && this.tabs.indexOf(entry) !== this.activeIndex) {
          this.notify(entry)
        }
      },
    })
    entry = { session, customTitle, autoTitle: null, hasActivity: false, el: null }
    this.paneHost.appendChild(session.el)
    this.tabs.push(entry)
    session.applyTheme(THEMES[this.settings.theme] ?? THEMES['dark']!, this.settings.fontSize)
    session.connect()
    return entry
  }

  newTab(): void {
    // M6: open the new tab in the active tab's current directory, if known.
    const cwd = this.tabs[this.activeIndex]?.session.cwd ?? undefined
    this.addEntry(null, null, cwd)
    this.persist()
    this.rebuild()
    this.activate(this.tabs.length - 1)
  }

  /** O2: open a new tab in `cwd` and run `claude --resume <id>`. */
  newTabForResume(cwd: string, sessionId: string): void {
    this.addEntry(null, null, cwd || undefined, `claude --resume ${sessionId}\r`)
    this.persist()
    this.rebuild()
    this.activate(this.tabs.length - 1)
  }

  /**
   * v0.6 (P6): spawn a new claude session in `repoPath`, labelled `repoName`.
   * If tabs with the same base name already exist, appends a #N suffix to
   * distinguish them (e.g. "web-terminal #2"). Mirrors newTabForResume in
   * structure: addEntry → persist → rebuild → activate.
   */
  openProject(repoPath: string, repoName: string, cmd = 'claude\r'): void {
    const n = this.countOpenWithTitlePrefix(repoName)
    const label = n === 0 ? repoName : `${repoName} #${n + 1}`
    this.addEntry(null, label, repoPath || undefined, cmd)
    this.persist()
    this.rebuild()
    this.activate(this.tabs.length - 1)
  }

  /** Count tabs whose customTitle is exactly `name` or starts with `name + ' #'`. */
  private countOpenWithTitlePrefix(name: string): number {
    return this.tabs.filter(
      (t) => t.customTitle === name || t.customTitle?.startsWith(`${name} #`) === true,
    ).length
  }

  activate(i: number): void {
    if (i < 0 || i >= this.tabs.length) return
    this.maybeAskNotify() // first switch is a user gesture — request notif permission
    this.activeIndex = i
    const entry = this.tabs[i]
    if (entry) entry.hasActivity = false // viewing clears the unread dot
    this.tabs.forEach((t, idx) => (idx === i ? t.session.show() : t.session.hide()))
    this.tabs.forEach((t) => this.refreshTab(t)) // in-place class/text update, no rebuild
    this.updateApprovalBar()
    this.persist()
  }

  closeTab(i: number): void {
    if (i < 0 || i >= this.tabs.length) return
    const [entry] = this.tabs.splice(i, 1)
    entry?.session.dispose()
    if (this.editingIndex === i) this.editingIndex = -1
    if (this.tabs.length === 0) {
      // v0.5: closing the last tab returns to the home screen — no auto-blank tab.
      this.activeIndex = -1
      this.persist()
      this.rebuild() // updateHomeView() (in rebuild) shows the chooser
      this.updateApprovalBar()
      return
    }
    const next = Math.min(i, this.tabs.length - 1)
    this.persist()
    this.rebuild()
    this.activate(next)
  }

  private moveTab(from: number, to: number): void {
    if (from < 0 || to < 0 || from === to || from >= this.tabs.length || to >= this.tabs.length) {
      return
    }
    const activeEntry = this.tabs[this.activeIndex]
    const [moved] = this.tabs.splice(from, 1)
    if (!moved) return
    this.tabs.splice(to, 0, moved)
    if (activeEntry) this.activeIndex = this.tabs.indexOf(activeEntry)
    this.persist()
    this.rebuild()
  }

  private commitRename(i: number, value: string): void {
    const entry = this.tabs[i]
    if (entry) {
      const trimmed = value.trim()
      entry.customTitle = trimmed.length > 0 ? trimmed : null // empty → revert to auto
    }
    this.editingIndex = -1
    this.persist()
    this.rebuild()
  }

  /** Route key-bar input to the currently active tab. */
  sendToActive(data: string): void {
    this.tabs[this.activeIndex]?.session.send(data)
  }

  /** Re-assert the active tab's size (latest-writer-wins) when this device
   *  regains focus, so a shared session snaps back to this screen's size. */
  refitActive(): void {
    this.tabs[this.activeIndex]?.session.refit()
  }

  /** Apply theme + font settings to every terminal (M3). */
  applySettings(s: Settings): void {
    this.settings = s
    const theme = THEMES[s.theme] ?? THEMES['dark']!
    for (const t of this.tabs) t.session.applyTheme(theme, s.fontSize)
  }

  /** A read-only view of all tabs for the dashboard (M7). */
  snapshot(): Array<{
    idx: number
    title: string
    conn: string
    claude: string
    active: boolean
    pending: boolean
  }> {
    return this.tabs.map((t, idx) => ({
      idx,
      title: this.displayTitle(t, idx),
      conn: t.session.status,
      claude: t.session.claudeStatus,
      active: idx === this.activeIndex,
      pending: t.session.pendingApproval,
    }))
  }

  focusTab(idx: number): void {
    this.activate(idx)
  }

  /** Scrollback search in the active tab (M1). */
  findInActive(query: string, dir: 'next' | 'prev'): void {
    const s = this.tabs[this.activeIndex]?.session
    if (!s) return
    if (dir === 'next') s.findNext(query)
    else s.findPrevious(query)
  }

  clearActiveSearch(): void {
    this.tabs[this.activeIndex]?.session.clearSearch()
  }

  /** Ask for notification permission once, on a user gesture (H2/H4). */
  private maybeAskNotify(): void {
    if (this.notifyAsked) return
    this.notifyAsked = true
    if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
      void Notification.requestPermission()
    }
  }

  /** Browser notification for a background tab needing approval. */
  private notify(entry: TabEntry): void {
    if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return
    const title = this.displayTitle(entry, this.tabs.indexOf(entry))
    new Notification(`Claude needs you — ${title}`, { body: 'Waiting for approval' })
  }

  /* ── rendering ───────────────────────────────────────────────────── */

  /** In-place update of one tab's classes/label/dot — never destroys the DOM. */
  private refreshTab(entry: TabEntry): void {
    const el = entry.el
    if (!el) return
    const idx = this.tabs.indexOf(entry)
    el.classList.toggle('active', idx === this.activeIndex)
    el.classList.toggle('unread', entry.hasActivity && idx !== this.activeIndex)
    const cs = entry.session.claudeStatus
    el.classList.toggle('claude-waiting', cs === 'waiting' && idx !== this.activeIndex)
    const title = this.displayTitle(entry, idx)
    el.title = title
    const dot = el.querySelector('.tab-dot')
    if (dot) dot.className = `tab-dot dot-${entry.session.status}`
    const label = el.querySelector('.tab-label')
    if (label) label.textContent = title
    const claude = el.querySelector('.tab-claude')
    if (claude) {
      claude.textContent =
        cs === 'working' ? '⚙' : cs === 'waiting' ? '⏳' : cs === 'idle' ? '✓' : ''
    }
  }

  /** Full rebuild — ONLY for structural changes (add/close/reorder/rename). */
  private rebuild(): void {
    this.tabBar.replaceChildren()

    this.tabs.forEach((entry, idx) => {
      const tabEl = document.createElement('div')
      entry.el = tabEl
      tabEl.className = 'tab'

      // Switch on pointerdown (fires before any drag; reliable with draggable, works on touch).
      tabEl.addEventListener('pointerdown', () => this.activate(idx))
      // Middle-click closes.
      tabEl.addEventListener('auxclick', (e) => {
        if (e.button === 1) {
          e.preventDefault()
          this.closeTab(idx)
        }
      })

      // Drag-to-reorder (disabled while renaming).
      if (idx !== this.editingIndex) {
        tabEl.draggable = true
        tabEl.addEventListener('dragstart', (e) => {
          this.dragIndex = idx
          e.dataTransfer?.setData('text/plain', String(idx))
          tabEl.classList.add('dragging')
        })
        tabEl.addEventListener('dragover', (e) => {
          e.preventDefault()
          tabEl.classList.add('drag-over')
        })
        tabEl.addEventListener('dragleave', () => tabEl.classList.remove('drag-over'))
        tabEl.addEventListener('drop', (e) => {
          e.preventDefault()
          tabEl.classList.remove('drag-over')
          this.moveTab(this.dragIndex, idx)
        })
        tabEl.addEventListener('dragend', () => {
          this.dragIndex = -1
          tabEl.classList.remove('dragging')
        })
      }

      const dot = document.createElement('span')
      dot.className = `tab-dot dot-${entry.session.status}`
      tabEl.appendChild(dot)

      if (idx === this.editingIndex) {
        const input = document.createElement('input')
        input.className = 'tab-rename'
        input.value = this.displayTitle(entry, idx)
        input.addEventListener('keydown', (e) => {
          if (e.key === 'Enter') this.commitRename(idx, input.value)
          else if (e.key === 'Escape') {
            this.editingIndex = -1
            this.rebuild()
          }
          e.stopPropagation()
        })
        input.addEventListener('blur', () => this.commitRename(idx, input.value))
        input.addEventListener('pointerdown', (e) => e.stopPropagation()) // don't re-activate
        tabEl.appendChild(input)
        requestAnimationFrame(() => {
          input.focus()
          input.select()
        })
      } else {
        const label = document.createElement('span')
        label.className = 'tab-label'
        label.textContent = this.displayTitle(entry, idx)
        label.addEventListener('dblclick', () => {
          this.editingIndex = idx
          this.rebuild()
        })
        tabEl.appendChild(label)

        const claude = document.createElement('span')
        claude.className = 'tab-claude'
        tabEl.appendChild(claude)

        const close = document.createElement('button')
        close.className = 'tab-close'
        close.textContent = '×'
        close.title = 'Close tab (the shell keeps running on the server)'
        close.setAttribute('aria-label', 'Close tab')
        close.addEventListener('pointerdown', (e) => e.stopPropagation())
        close.addEventListener('click', (e) => {
          e.stopPropagation()
          this.closeTab(idx)
        })
        tabEl.appendChild(close)
      }

      // Apply current active/unread state.
      this.refreshTab(entry)
      this.tabBar.appendChild(tabEl)
    })

    const add = document.createElement('button')
    add.className = 'tab-add'
    add.textContent = '+'
    add.title = 'New session'
    add.setAttribute('aria-label', 'New session')
    add.addEventListener('click', () => this.newTab())
    this.tabBar.appendChild(add)

    // Show/hide the home screen based on whether any tab is open.
    this.updateHomeView()
  }
}
