/**
* GET /search?q=...&type=repo|code
*
* Two modes:
* - repo: matches repo names + descriptions (search-all-repos handler)
* - code: git grep across repos (search-code-all handler)
*
* Both backends are already wired up for the nav autocomplete; the full
* search page dispatches through the same handlers and renders paged
* results with match highlighting.
*/
import { layout, escapeHtml } from '../helpers.js'
const SEARCH_STYLES = `
<style>
.search-tabs { display: flex; gap: 4px; margin-bottom: 1rem; border-bottom: 1px solid #30363d; }
.search-tabs a { padding: 0.5rem 1rem; color: #8b949e; border-bottom: 2px solid transparent; text-decoration: none; font-size: 0.9rem; }
.search-tabs a:hover { color: #c9d1d9; }
.search-tabs a.active { color: #c9d1d9; border-bottom-color: #58a6ff; }
.search-form { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.search-form input { flex: 1; }
.hl { background: #5e4b00; color: #f0e68c; padding: 1px 2px; border-radius: 2px; }
.code-match { font-family: 'SF Mono', monospace; font-size: 0.78rem; color: #8b949e; background: #0d1117; padding: 3px 6px; border-radius: 3px; margin-top: 3px; display: block; white-space: pre; overflow-x: auto; }
.code-match-header { color: #58a6ff; font-size: 0.8rem; font-weight: 500; }
.code-match-line { color: #6e7681; font-family: monospace; font-size: 0.72rem; margin-right: 0.5rem; }
</style>`
export async function renderGlobalSearch (svc, ctx, query, type) {
const prefix = (ctx && ctx.prefix) || ''
query = (query || '').trim()
type = type === 'code' ? 'code' : 'repo' // default to repo mode
const activeRepo = type === 'repo' ? 'active' : ''
const activeCode = type === 'code' ? 'active' : ''
const qEnc = encodeURIComponent(query)
let resultsHtml = ''
if (query.length >= 2) {
if (type === 'repo') {
const res = await svc.dispatch('search-all-repos', { query, limit: 50 }, ctx).catch(() => ({ repos: [] }))
resultsHtml = renderRepoResults(res, query, prefix)
} else {
const res = await svc.dispatch('search-code-all', { query, limit: 100, maxRepos: 20 }, ctx).catch(() => ({ results: [] }))
resultsHtml = renderCodeResults(res, query, prefix)
}
} else if (query.length === 1) {
resultsHtml = '<div class="card"><div class="empty">Enter at least 2 characters.</div></div>'
}
const body = `
${SEARCH_STYLES}
<h1>search</h1>
<div class="search-tabs">
<a href="${prefix}/search?q=${qEnc}&type=repo" class="${activeRepo}">Names</a>
<a href="${prefix}/search?q=${qEnc}&type=code" class="${activeCode}">Content</a>
</div>
<form method="GET" action="${prefix}/search" class="search-form">
<input type="hidden" name="type" value="${type}">
<input name="q" value="${escapeHtml(query)}" placeholder="${type === 'code' ? 'Search code across all repos...' : 'Search repo names and descriptions...'}">
<button type="submit">Search</button>
</form>
${resultsHtml}
`
return layout(query ? 'search: ' + query : 'search', '/search', body, ctx)
}
function renderRepoResults (res, query, prefix = '') {
const repos = res.repos || []
if (repos.length === 0) {
return '<div class="card"><div class="empty">No repos matched "' + escapeHtml(query) + '".</div></div>'
}
return `
<div class="card">
<h2>${repos.length} repo${repos.length === 1 ? '' : 's'}</h2>
${repos.map(r => `
<div class="item">
<div>
<a class="title" href="${prefix}/repos/${encodeURIComponent(r.name)}">${highlight(escapeHtml(r.name), query)}</a>
${r.description ? '<div class="meta" style="margin-top:0.2rem">' + highlight(escapeHtml(r.description), query) + '</div>' : ''}
</div>
<div class="meta">${r.commits || 0} commit${r.commits === 1 ? '' : 's'} · ${r.branches || 0} branch${r.branches === 1 ? '' : 'es'}</div>
</div>`).join('')}
</div>`
}
function renderCodeResults (res, query, prefix = '') {
const results = res.results || []
if (results.length === 0) {
return '<div class="card"><div class="empty">No code matches for "' + escapeHtml(query) + '".</div></div>'
}
const totalMatches = res.totalMatches || 0
const reposSearched = res.reposSearched || 0
return `
<div class="card">
<h2>${totalMatches} match${totalMatches === 1 ? '' : 'es'} in ${results.length} repo${results.length === 1 ? '' : 's'} <span class="meta" style="font-size:0.75rem">(searched ${reposSearched} repo${reposSearched === 1 ? '' : 's'})</span></h2>
</div>
${results.map(repoResult => `
<div class="card">
<h2><a href="${prefix}/repos/${encodeURIComponent(repoResult.repo)}">${escapeHtml(repoResult.repo)}</a> <span class="meta" style="font-size:0.75rem">${repoResult.matches.length} match${repoResult.matches.length === 1 ? '' : 'es'}</span></h2>
${repoResult.matches.slice(0, 20).map(m => `
<div class="item">
<div>
<a class="code-match-header" href="${prefix}/repos/${encodeURIComponent(repoResult.repo)}/blob/${encodeURIComponent(m.file)}#L${m.line}">${escapeHtml(m.file)}</a>
</div>
<code class="code-match"><span class="code-match-line">${m.line}:</span>${highlight(escapeHtml(m.content), query)}</code>
</div>`).join('')}
${repoResult.matches.length > 20 ? `<div class="meta" style="margin-top:0.5rem">+ ${repoResult.matches.length - 20} more matches — <a href="${prefix}/repos/${encodeURIComponent(repoResult.repo)}/search?q=${encodeURIComponent(query)}">view in repo</a></div>` : ''}
</div>`).join('')}`
}
function highlight (escaped, query) {
if (!query) return escaped
// Escape regex metacharacters in the query
const safe = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&')
const re = new RegExp('(' + safe + ')', 'gi')
return escaped.replace(re, '<span class="hl">$1</span>')
}