/**
 * public/projects.ts — Projects panel for the home screen (v0.6).
 *
 * Renders a grid of discovered git repositories fetched from GET /projects.
 * Each card shows: repo name, branch chip, dirty indicator, last-active time,
 * and running sessions (1:N). Supports live search and per-project favourites
 * persisted to localStorage.
 *
 * Pure helpers (filterProjects, sortProjects, getFavs, saveFavs, toggleFav)
 * are exported for unit testing without DOM. mountProjects is the thin wiring.
 *
 * Timer lifecycle mirrors launcher.ts: auto-refresh every 5 s while visible,
 * stopped + cleaned up on hide.
 */

import type { ProjectInfo, ProjectSessionRef, ClaudeStatus } from '../src/types.js'
import { el, relTime } from './preview-grid.js'

/* ── Constants ───────────────────────────────────────────────────────────── */

const FAV_KEY = 'web-terminal:proj-favs'
const REFRESH_MS = 5000

/* ── Public contract (consumed by P6 — tabs.ts wiring) ──────────────────── */

export interface ProjectsHooks {
  /** Spawn a new claude session in the given repo. */
  onOpenProject: (repoPath: string, repoName: string) => void
  /** Enter an already-running session by id. */
  onEnterSession: (id: string) => void
}

export interface ProjectsPanel {
  setVisible(v: boolean): void
  refresh(): void
}

/* ── Pure helpers (exported for unit tests) ──────────────────────────────── */

/**
 * Filter projects by name or path substring (case-insensitive).
 * Returns a new array; never mutates the input.
 */
export function filterProjects(projects: readonly ProjectInfo[], query: string): ProjectInfo[] {
  const trimmed = query.trim()
  if (!trimmed) return [...projects]
  const lower = trimmed.toLowerCase()
  return projects.filter(
    (p) => p.name.toLowerCase().includes(lower) || p.path.toLowerCase().includes(lower),
  )
}

/**
 * Sort projects: favourites first, then by lastActiveMs descending.
 * Returns a new array; never mutates the input.
 */
export function sortProjects(
  projects: readonly ProjectInfo[],
  favs: ReadonlySet<string>,
): ProjectInfo[] {
  return [...projects].sort((a, b) => {
    const aFav = favs.has(a.path) ? 0 : 1
    const bFav = favs.has(b.path) ? 0 : 1
    if (aFav !== bFav) return aFav - bFav
    return (b.lastActiveMs ?? 0) - (a.lastActiveMs ?? 0)
  })
}

/** Load persisted favourite project paths from localStorage. */
export function getFavs(): Set<string> {
  try {
    const raw = localStorage.getItem(FAV_KEY)
    if (!raw) return new Set()
    const parsed: unknown = JSON.parse(raw)
    // Never trust persisted data: keep only string entries (corrupt/tampered
    // localStorage could otherwise poison the Set<string> used for path matching).
    if (!Array.isArray(parsed)) return new Set()
    return new Set(parsed.filter((x): x is string => typeof x === 'string'))
  } catch {
    return new Set()
  }
}

/** Persist a set of favourite project paths to localStorage. */
export function saveFavs(favs: ReadonlySet<string>): void {
  try {
    localStorage.setItem(FAV_KEY, JSON.stringify([...favs]))
  } catch {
    // localStorage unavailable — run without persistence
  }
}

/**
 * Immutable toggle: add if absent, remove if present.
 * Returns a new Set; never mutates the original.
 */
export function toggleFav(favs: ReadonlySet<string>, path: string): Set<string> {
  const next = new Set(favs)
  if (next.has(path)) {
    next.delete(path)
  } else {
    next.add(path)
  }
  return next
}

/* ── Fetch ───────────────────────────────────────────────────────────────── */

/** Coerce one untrusted /projects element into a safe ProjectInfo, or null.
 *  Guarantees `sessions` is always an array so card rendering never throws on a
 *  malformed response (never trust external data — API responses included). */
export function normalizeProject(p: unknown): ProjectInfo | null {
  if (p === null || typeof p !== 'object') return null
  const o = p as Record<string, unknown>
  if (typeof o['name'] !== 'string' || typeof o['path'] !== 'string') return null
  const sessions = Array.isArray(o['sessions']) ? (o['sessions'] as ProjectSessionRef[]) : []
  return {
    name: o['name'],
    path: o['path'],
    isGit: o['isGit'] === true,
    ...(typeof o['branch'] === 'string' ? { branch: o['branch'] } : {}),
    ...(typeof o['dirty'] === 'boolean' ? { dirty: o['dirty'] } : {}),
    ...(typeof o['lastActiveMs'] === 'number' ? { lastActiveMs: o['lastActiveMs'] } : {}),
    sessions,
  }
}

async function fetchProjects(): Promise<ProjectInfo[]> {
  try {
    const res = await fetch('/projects')
    const data: unknown = await res.json()
    if (!Array.isArray(data)) return []
    return data.map(normalizeProject).filter((x): x is ProjectInfo => x !== null)
  } catch {
    return []
  }
}

/* ── Status helpers ──────────────────────────────────────────────────────── */

function sessionDotClass(status: ClaudeStatus): string {
  return `proj-session-dot proj-dot-${status}`
}

/* ── Card sub-elements ───────────────────────────────────────────────────── */

function makeSessionRow(
  sess: { id: string; title?: string; status: ClaudeStatus; clientCount: number },
  onEnterSession: (id: string) => void,
): HTMLElement {
  const row = el('div', 'proj-session-row')
  row.setAttribute('role', 'button')
  row.tabIndex = 0

  const dot = el('span', sessionDotClass(sess.status))
  const title = el('span', 'proj-session-title', sess.title ?? 'claude')
  const badge = el('span', 'proj-session-badge', `👁 ${sess.clientCount}`)

  row.append(dot, title, badge)

  row.addEventListener('click', () => onEnterSession(sess.id))
  row.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      onEnterSession(sess.id)
    }
  })

  return row
}

function makeProjectCard(
  project: ProjectInfo,
  favs: ReadonlySet<string>,
  hooks: ProjectsHooks,
  onToggleFav: (path: string) => void,
): HTMLElement {
  const card = el('div', 'proj-card')

  // ── Header ────────────────────────────────────────────────────────────────
  const header = el('div', 'proj-card-head')

  const isFav = favs.has(project.path)
  const favBtn = el('button', isFav ? 'proj-fav on' : 'proj-fav')
  favBtn.textContent = '★'
  favBtn.title = isFav ? 'Remove from favourites' : 'Add to favourites'
  favBtn.addEventListener('click', (e) => {
    e.stopPropagation()
    onToggleFav(project.path)
  })

  const name = el('span', 'proj-name', project.name)

  header.append(favBtn, name)

  if (project.branch) {
    const branch = el('span', 'proj-branch', project.branch)
    header.append(branch)
  }

  if (project.dirty === true) {
    const dirty = el('span', 'proj-dirty', '●')
    dirty.title = 'Uncommitted changes'
    header.append(dirty)
  }

  card.append(header)

  // ── Meta line ─────────────────────────────────────────────────────────────
  if (project.lastActiveMs !== undefined) {
    const meta = el('div', 'proj-meta', `active ${relTime(project.lastActiveMs)} ago`)
    card.append(meta)
  }

  // ── Sessions area (1:N) ───────────────────────────────────────────────────
  const runningSessions = project.sessions.filter((s) => !s.exited)

  if (runningSessions.length === 0) {
    // 0 running sessions: single primary "start claude here" action
    const newBtn = el('button', 'proj-new proj-new-primary', '＋ start claude here')
    newBtn.addEventListener('click', () => hooks.onOpenProject(project.path, project.name))
    card.append(newBtn)
  } else {
    // 1+ running sessions: list session rows + always-present "＋ New" button
    const sessionsList = el('div', 'proj-sessions-list')
    for (const sess of runningSessions) {
      sessionsList.append(makeSessionRow(sess, hooks.onEnterSession))
    }
    card.append(sessionsList)

    const newBtn = el('button', 'proj-new', '＋ New')
    newBtn.addEventListener('click', () => hooks.onOpenProject(project.path, project.name))
    card.append(newBtn)
  }

  return card
}

/* ── Mount ───────────────────────────────────────────────────────────────── */

export function mountProjects(host: HTMLElement, hooks: ProjectsHooks): ProjectsPanel {
  const root = el('div', 'proj-panel')
  root.style.display = 'none'
  host.appendChild(root)

  // Search box
  const searchWrap = el('div', 'proj-search-wrap')
  const searchInput = document.createElement('input')
  searchInput.className = 'proj-search'
  searchInput.type = 'search'
  searchInput.placeholder = 'Filter projects…'
  searchInput.setAttribute('aria-label', 'Filter projects')
  searchWrap.append(searchInput)
  root.append(searchWrap)

  const grid = el('div', 'proj-grid')
  root.append(grid)

  let allProjects: ProjectInfo[] = []
  let favs: Set<string> = getFavs()
  let timer: ReturnType<typeof setInterval> | null = null

  function renderGrid(): void {
    const filtered = filterProjects(allProjects, searchInput.value)
    const sorted = sortProjects(filtered, favs)

    grid.replaceChildren()

    if (sorted.length === 0) {
      const msg =
        allProjects.length === 0
          ? 'No projects found. Check PROJECT_ROOTS configuration.'
          : 'No projects match your search.'
      grid.append(el('div', 'proj-empty', msg))
      return
    }

    for (const p of sorted) {
      grid.append(
        makeProjectCard(p, favs, hooks, (path) => {
          favs = toggleFav(favs, path)
          saveFavs(favs)
          renderGrid()
        }),
      )
    }
  }

  async function refresh(): Promise<void> {
    allProjects = await fetchProjects()
    favs = getFavs()
    renderGrid()
  }

  searchInput.addEventListener('input', () => renderGrid())

  return {
    setVisible(v: boolean): void {
      root.style.display = v ? 'block' : 'none'
      if (v) {
        void refresh()
        if (timer === null) {
          timer = setInterval(() => {
            if (root.style.display !== 'none') void refresh()
          }, REFRESH_MS)
        }
      } else {
        if (timer !== null) {
          clearInterval(timer)
          timer = null
        }
      }
    },
    refresh(): void {
      void refresh()
    },
  }
}
