<?php
declare(strict_types=1);

/*
 * --------------------------------------------------------------------------

/**
 * search.php
 *
 * Self-contained legal opinion search UI, backed by Meilisearch, designed to
 * be embedded via <iframe> inside Moodle Workplace 5.0. Also handles two
 * student-facing features backed by our own local sidecar database (never
 * Moodle's): saving cases for later, and requesting a physical print from
 * staff. See schema.sql / lib.php for the data layer, and config.php for
 * the student-identity trust model (USERNAME_TRUST_MODE).
 *
 * DESIGN NOTES -- read before deploying
 * --------------------------------------------------------------------------
 * This tool will be used by incarcerated individuals doing their own legal
 * research, often over restricted facility networks and shared/kiosk
 * hardware. That changed several defaults from what a typical "modern web
 * app" brief would produce:
 *
 *  - NO external requests of any kind (no CDN CSS/JS, no Google Fonts, no
 *    icon fonts, no analytics/tracking pixels). Correctional-facility
 *    networks are usually a locked-down allowlist; anything not self-hosted
 *    is likely to simply fail to load. It also means nothing about a
 *    person's search terms is ever leaked to a third-party host via
 *    referrer headers.
 *  - Works with JavaScript OFF for search, filters, pagination, saving
 *    cases, and requesting prints -- all plain GET/POST forms and links.
 *    A little vanilla JS is included only as a progressive-enhancement
 *    convenience; nothing depends on it.
 *  - No PHP session for the student-facing flow (see config.php re: the
 *    USERNAME_TRUST_MODE tradeoffs this implies). Save/Request-Print POST
 *    actions are CSRF-protected via a non-identifying double-submit cookie
 *    instead of a session -- see lib.php.
 *  - Opinion text is rendered as escaped plain text everywhere (never as
 *    raw HTML), and is already stripped of all tags/links/images at
 *    ingestion time -- so an embedded Wikimedia image or citation link in
 *    source HTML can never load or become clickable here. See
 *    strip_external_references() in lib.php and clean_html() in
 *    ingest_courtlistener.py.
 *  - Large base font size, high contrast, visible focus outlines, big tap
 *    targets, <meta name="robots" content="noindex, nofollow">.
 *  - Does NOT send X-Frame-Options (would break the Moodle iframe embed).
 *    Restrict embedding via a CSP frame-ancestors directive scoped to your
 *    Moodle origin instead -- see TODO below.
 *
 * SETUP
 * --------------------------------------------------------------------------
 *  1. composer require meilisearch/meilisearch-php
 *  2. Fill in config.php.
 *  3. Make sure meilisearch_index_settings.json has been applied to your
 *     index (see ingest_courtlistener.py --apply-settings).
 */

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/lib.php';

// TODO: once you know the exact Moodle Workplace origin this will be framed
// from, lock the embed down, e.g.:
//   header("Content-Security-Policy: frame-ancestors 'self' https://moodle.yourfacility.example");
// header("Content-Security-Policy: frame-ancestors 'self' https://YOUR-MOODLE-DOMAIN;");

header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
header('Content-Type: text/html; charset=UTF-8');

error_reporting(E_ALL);
ini_set('display_errors', '0'); // never leak stack traces / server paths to the browser

if (!file_exists(VENDOR_AUTOLOAD)) {
    http_response_code(500);
    echo '<!doctype html><meta charset="utf-8"><p>Search is temporarily unavailable. '
       . '(Composer dependencies are not installed on the server.)</p>';
    exit;
}
require_once VENDOR_AUTOLOAD;

use Meilisearch\Client;

/** Helper for filter checkbox rendering. */
function is_checked(array $selected, string $value): bool
{
    return in_array($value, $selected, true);
}

/**
 * Safely render Meilisearch highlighting in plain text.
 *
 * Escape the entire value first, then restore only the exact <mark> tags
 * inserted by the search options below. All source text remains escaped.
 */
function highlighted_text(string $text): string
{
    return str_replace(
        ['&lt;mark&gt;', '&lt;/mark&gt;'],
        ['<mark>', '</mark>'],
        h($text)
    );
}

// ---------------------------------------------------------------------------
// Identity + CSRF (must happen before any output -- setcookie() needs that)
// ---------------------------------------------------------------------------
$username = resolve_student_identity();
$csrfToken = ensure_csrf_cookie_token();


// ---------------------------------------------------------------------------
// Handle POST actions: Save Case / Unsave Case / Request Print
// ---------------------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $csrfOk = verify_csrf_cookie_token();
    $action = is_string($_POST['action'] ?? null) ? $_POST['action'] : '';
    $opinionId = is_string($_POST['opinion_id'] ?? null) ? $_POST['opinion_id'] : '';

    if ($csrfOk && $username !== null && preg_match('/^[A-Za-z0-9_-]{1,64}$/', $opinionId)) {
        $caseName  = mb_substr(trim((string) ($_POST['case_name'] ?? '')), 0, 500) ?: null;
        $courtName = mb_substr(trim((string) ($_POST['court_name'] ?? '')), 0, 255) ?: null;
        $dateFiled = mb_substr(trim((string) ($_POST['date_filed'] ?? '')), 0, 32) ?: null;

        switch ($action) {
            case 'save_case':
                save_case($username, $opinionId, $caseName, $courtName, $dateFiled);
                break;
            case 'unsave_case':
                unsave_case($username, $opinionId);
                break;
            case 'request_print':
                create_print_request($username, $opinionId, $caseName, $courtName, $dateFiled);
                break;
        }
    }
    // Post/Redirect/Get: bounce back to the same page (same $_GET context,
    // e.g. q/page/filters/view) as a plain GET so refreshing never
    // re-submits the action. Intentionally NOT built from any user-supplied
    // URL -- only from the already-validated $_GET superglobal -- so this
    // can't be turned into an open redirect.
    $returnQuery = http_build_query($_GET);
    header('Location: https://lawlibrary.maxxlms.com/search.php' . ($returnQuery !== '' ? '?' . $returnQuery : ''));
    exit;
}

// ---------------------------------------------------------------------------
// Read + validate GET input
// ---------------------------------------------------------------------------
$query = trim($_GET['q'] ?? '');
$page = (int)($_GET['page'] ?? 1);
$selectedCourts = $_POST['courts'] ?? $_GET['courts'] ?? [];
$selectedStatus = $_POST['status'] ?? $_GET['status'] ?? [];

if (!is_array($selectedCourts)) {
    $selectedCourts = [];
}
if (!is_array($selectedStatus)) {
    $selectedStatus = [];
}

$currentYear = (int) date('Y');
$yearFrom = (int)($_POST['year_from'] ?? $_GET['year_from'] ?? 0);
$yearTo = (int)($_POST['year_to'] ?? $_GET['year_to'] ?? 0);
$isSavedView = isset($_GET['saved']) && $_GET['saved'] === '1';
$viewId = (!$isSavedView && isset($_GET['view']) && is_string($_GET['view']) && preg_match('/^[A-Za-z0-9_-]{1,64}$/', $_GET['view'])) ? $_GET['view'] : null;

// ---------------------------------------------------------------------------
// Meilisearch client
// ---------------------------------------------------------------------------
$client = new Client(MEILI_HOST, MEILI_API_KEY !== '' ? MEILI_API_KEY : null);
$index  = $client->index(MEILI_INDEX);

$errorMessage = null;

// ---------------------------------------------------------------------------
// Detail view: a single opinion, full text
// ---------------------------------------------------------------------------
$viewDoc = null;
if ($viewId !== null) {
    try {
        $viewDoc = $index->getDocument($viewId);
    } catch (\Throwable $e) {
        // Covers both a genuine "not found" and a Meilisearch/network outage --
        // either way we show the same friendly not-found message below rather
        // than leaking internal error detail.
        $viewDoc = null;
        error_log('[search.php] getDocument error: ' . $e->getMessage());
    }
}

// ---------------------------------------------------------------------------
// Saved-cases view
// ---------------------------------------------------------------------------
$savedCases = [];
if ($isSavedView && $username !== null) {
    $savedCases = list_saved_cases($username);
}

// ---------------------------------------------------------------------------
// Search: build filter expression from validated/whitelisted input
// ---------------------------------------------------------------------------
$results = null;
$facetDistribution = [];
$totalHits = 0;
$totalPages = 1;

if ($viewId === null && !$isSavedView) {
    $filterParts = [];
    
    if (!empty($selectedCourts) && is_array($selectedCourts)) {
        $courtFilters = [];
        foreach ($selectedCourts as $court) {
            $courtFilters[] = "court_name = \"" . addslashes((string)$court) . "\"";
        }
        if (!empty($courtFilters)) {
            $filterParts[] = '(' . implode(' OR ', $courtFilters) . ')';
        }
    }
    
    if (!empty($selectedStatus) && is_array($selectedStatus)) {
        $statusFilters = [];
        foreach ($selectedStatus as $status) {
            $statusFilters[] = "precedential_status = \"" . addslashes((string)$status) . "\"";
        }
        if (!empty($statusFilters)) {
            $filterParts[] = '(' . implode(' OR ', $statusFilters) . ')';
        }
    }
    
    if ($yearFrom > 0) {
        $filterParts[] = 'year >= ' . $yearFrom;
    }
    if ($yearTo > 0) {
        $filterParts[] = 'year <= ' . $yearTo;
    }


    $filterExpression = implode(' AND ', $filterParts);

    // Build search options as a plain array. Note: the Meilisearch PHP SDK's
    // SearchQuery::setFilter() is NOT nullable -- passing 'filter' => null
    // here (instead of omitting the key entirely) throws a TypeError, so we
    // only add the key when we actually have a filter expression.
    $searchOptions = [
        'page'                  => $page,
        'hitsPerPage'           => RESULTS_PER_PAGE,
        'facets'                => ['court_name', 'precedential_status'],
        'attributesToHighlight' => ['case_name', 'text'],
        'attributesToCrop'      => ['text'],
        'cropLength'            => SNIPPET_CROP_WORDS,
        'highlightPreTag'       => '<mark>',
        'highlightPostTag'      => '</mark>',
        'attributesToRetrieve'  => ['id', 'case_name', 'court_name', 'date_filed', 'year', 'precedential_status'],
    ];
    if ($filterExpression !== '') {
        $searchOptions['filter'] = $filterExpression;
    }

    try {
        $searchResult = $index->search($query !== '' ? $query : null, $searchOptions);
        $raw = $searchResult->toArray();
        $results = $raw['hits'] ?? [];
        $facetDistribution = $raw['facetDistribution'] ?? [];
        $totalHits = (int) ($raw['totalHits'] ?? 0);
        $totalPages = max(1, (int) ($raw['totalPages'] ?? 1));
    } catch (\Throwable $e) {
        // Broad catch on purpose: covers Meilisearch API errors (ApiException),
        // the server being unreachable (CommunicationException), and anything
        // else unexpected. In every case we degrade to the same friendly
        // message rather than leaking server/config details to the browser.
        $errorMessage = 'Search is temporarily unavailable. Please try again in a moment.';
        error_log('[search.php] Meilisearch error: ' . $e->getMessage());
    }
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Legal Opinion Search</title>
<style>
  /* All styling is self-contained -- no external stylesheets or fonts. */
  :root {
    --color-bg: #f7f7f5;
    --color-surface: #ffffff;
    --color-border: #d6d3ce;
    --color-text: #1a1a1a;
    --color-muted: #4b4b4b;
    --color-primary: #1c4e80;
    --color-primary-dark: #123452;
    --color-focus: #ff8c00;
    --color-mark-bg: #fff1a8;
    --color-success: #1e6b3c;
    --radius: 6px;
  }
  * { box-sizing: border-box; }
  html, body {
    margin: 0;
    padding: 0;
    background: var(--color-bg);
    color: var(--color-text);
    font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 18px;
    line-height: 1.5;
  }
  a { color: var(--color-primary); }
  a:hover { color: var(--color-primary-dark); }
  *:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }
  .skip-link {
    position: absolute; left: -9999px; top: 0; background: #000; color: #fff;
    padding: 10px 16px; z-index: 100;
  }
  .skip-link:focus { left: 8px; top: 8px; }

  header.site-header {
    background: var(--color-primary);
    color: #fff;
    padding: 16px 20px;
  }
  header.site-header h1 {
    margin: 0 0 12px 0;
    font-size: 1.5rem;
  }
  .header-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 12px;
  }
  .identity-note {
    font-size: 0.9rem;
    color: #e6eef6;
  }
  .header-links a {
    color: #fff;
    font-weight: 600;
    text-decoration: underline;
    margin-left: 16px;
  }

  form.search-form {
    display: flex;
    gap: 8px;
    max-width: 900px;
  }
  .search-form input[type="search"] {
    flex: 1;
    font-size: 1.1rem;
    padding: 12px 14px;
    border: 2px solid transparent;
    border-radius: var(--radius);
  }
  .search-form button {
    font-size: 1.05rem;
    font-weight: 600;
    padding: 12px 20px;
    border: none;
    border-radius: var(--radius);
    background: #ffffff;
    color: var(--color-primary-dark);
    cursor: pointer;
    min-height: 44px;
  }
  .search-form button:hover { background: #eaeaea; }
  .search-scope-note {
    max-width: 900px;
    margin: 10px 0 0 0;
    color: #e6eef6;
    font-size: 0.85rem;
    line-height: 1.4;
  }



  .site-footer {
    border-top: 1px solid var(--color-border);
    background: var(--color-surface);
    color: #4b5563;
    padding: 16px 20px;
    text-align: center;
    font-size: 0.85rem;
  }

  .layout {
    display: flex;
    gap: 20px;
    max-width: 1200px;
    margin: 20px auto;
    padding: 0 16px 40px 16px;
    align-items: flex-start;
  }
  @media (max-width: 800px) {
    .layout { flex-direction: column; }
  }

  aside.filters {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 16px;
    width: 260px;
    flex-shrink: 0;
  }
  aside.filters h2 {
    font-size: 1.05rem;
    margin: 0 0 10px 0;
  }
  fieldset {
    border: none;
    border-top: 1px solid var(--color-border);
    padding: 12px 0;
    margin: 0 0 4px 0;
  }
  fieldset legend {
    font-weight: 700;
    padding: 0;
    margin-bottom: 8px;
  }
  .filter-option {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
    min-height: 32px;
  }
  .filter-option input[type="checkbox"] {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
  }
  .filter-option label { cursor: pointer; }
  .facet-count { color: var(--color-muted); font-size: 0.9rem; }

  .year-range { display: flex; gap: 8px; align-items: center; }
  .year-range input[type="number"] {
    width: 90px;
    padding: 8px;
    font-size: 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
  }

  .apply-btn {
    width: 100%;
    padding: 10px;
    margin-top: 10px;
    font-size: 1rem;
    font-weight: 600;
    background: var(--color-primary);
    color: #fff;
    border: none;
    border-radius: var(--radius);
    cursor: pointer;
    min-height: 44px;
  }
  .apply-btn:hover { background: var(--color-primary-dark); }

  .clear-link { display: inline-block; margin-top: 10px; font-size: 0.95rem; }

  main.results { flex: 1; min-width: 0; }
  .results-summary { margin-bottom: 14px; color: var(--color-muted); }

  .result-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 16px 18px;
    margin-bottom: 14px;
  }
  .result-card h3 { margin: 0 0 6px 0; font-size: 1.2rem; }
  .result-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 14px;
    color: var(--color-muted);
    font-size: 0.95rem;
    margin-bottom: 10px;
  }
  .result-snippet { margin: 0 0 10px 0; }
  .result-snippet mark { background: var(--color-mark-bg); padding: 0 2px; }

  .action-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
    margin-top: 10px;
  }
  .view-link, .action-btn {
    display: inline-block;
    font-weight: 600;
    font-size: 0.95rem;
    padding: 8px 14px;
    border: 2px solid var(--color-primary);
    border-radius: var(--radius);
    text-decoration: none;
    min-height: 44px;
    line-height: 26px;
    background: #fff;
    color: var(--color-primary);
    cursor: pointer;
  }
  .view-link:hover, .action-btn:hover { background: var(--color-primary); color: #fff; }
  .action-btn.is-active {
    background: var(--color-success);
    border-color: var(--color-success);
    color: #fff;
  }
  .inline-form { display: inline; margin: 0; }

  .empty-state, .error-state {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 24px;
    text-align: center;
    color: var(--color-muted);
  }
  .error-state { border-color: #b3261e; color: #b3261e; background: #fdecea; }

  nav.pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 20px;
  }
  nav.pagination a, nav.pagination span {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    text-decoration: none;
    background: var(--color-surface);
  }
  nav.pagination a:hover { background: #eef3f8; }
  nav.pagination .current { background: var(--color-primary); color: #fff; border-color: var(--color-primary); font-weight: 700; }

  .opinion-detail {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 24px;
    max-width: 900px;
  }
  .opinion-detail h2 { margin-top: 0; }
  .opinion-text {
    white-space: pre-wrap;
    line-height: 1.7;
    font-size: 1.05rem;
    margin-top: 16px;
  }
  .back-link { display: inline-block; margin-bottom: 16px; font-weight: 600; }

  table.saved-table { width: 100%; border-collapse: collapse; background: var(--color-surface); }
  table.saved-table th, table.saved-table td {
    text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--color-border);
  }

  @media print {
    header.site-header, aside.filters, nav.pagination, .back-link, .action-row { display: none; }
  }
</style>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to results</a>

<header class="site-header">
  <div class="header-row">
<h1 style="margin:0;">Legal Opinion Search</h1>
<div>
    <?php if ($username !== null): ?>
        Browsing as: <strong><?= h($username) ?></strong></span>
        <span class="header-links"><a href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(['username' => $username, 'saved' => '1'])) ?>">My Saved Cases</a></span>
    <?php else: ?>
        Sign-in info not detected -- open this page from your course to save cases or request prints.</span>
    <?php endif; ?>
</div>
</div>
<form class="search-form" method="get" action="https://lawlibrary.maxxlms.com/search.php" role="search">
    <?php if ($username !== null): ?><input type="hidden" name="username" value="<?= h($username) ?>"><?php endif; ?>
    <label for="q" style="position:absolute; left:-9999px;">Search case law</label>
    <input
      type="search"
      id="q"
      name="q"
      value="<?= h($query) ?>"
      placeholder="Search case name, citation, or keywords..."
      maxlength="<?= (int) MAX_QUERY_LENGTH ?>"
      autocomplete="off"
    >
    <button type="submit">Search</button>

    <a class="clear-search-btn"
       href="https://lawlibrary.maxxlms.com/search.php<?= $username !== null ? '?' . h(http_build_query(['username' => $username])) : '' ?>">
       Clear search
    </a>

  </form>
  <p class="search-scope-note">
    This search returns judicial opinions available in this database only. It does not include statutes, regulations, court rules, briefs, filings, or legal forms.
  </p>
</header>

<div class="layout">

<?php
/**
 * Render the Save Case / Request Print buttons for one opinion. $doc needs
 * id, case_name, court_name, date_filed. No-op (renders nothing) if nobody
 * is identified.
 */
function render_case_actions(?string $username, array $doc, string $csrfToken): void
{
    if ($username === null) {
        return;
    }
    $opinionId = (string) ($doc['id'] ?? '');
    if ($opinionId === '') {
        return;
    }
    $alreadySaved  = is_case_saved($username, $opinionId);
    $alreadyQueued = has_pending_print_request($username, $opinionId);
    $hidden = ''
        . '<input type="hidden" name="csrf_token" value="' . h($csrfToken) . '">'
        . '<input type="hidden" name="opinion_id" value="' . h($opinionId) . '">'
        . '<input type="hidden" name="case_name" value="' . h((string) ($doc['case_name'] ?? '')) . '">'
        . '<input type="hidden" name="court_name" value="' . h((string) ($doc['court_name'] ?? '')) . '">'
        . '<input type="hidden" name="date_filed" value="' . h((string) ($doc['date_filed'] ?? '')) . '">';
    $postQuery = http_build_query($_GET);
    $postTarget = h('https://lawlibrary.maxxlms.com/search.php' . ($postQuery !== '' ? '?' . $postQuery : ''));
    ?>
      <form class="inline-form" method="post" action="<?= $postTarget ?>">
        <?= $hidden ?>
        <input type="hidden" name="action" value="<?= $alreadySaved ? 'unsave_case' : 'save_case' ?>">
        <button type="submit" class="action-btn<?= $alreadySaved ? ' is-active' : '' ?>">
          <?= $alreadySaved ? 'Saved &#10003; (remove)' : 'Save Case' ?>
        </button>
      </form>
      <form class="inline-form" method="post" action="<?= $postTarget ?>">
        <?= $hidden ?>
        <input type="hidden" name="action" value="request_print">
        <button type="submit" class="action-btn<?= $alreadyQueued ? ' is-active' : '' ?>" <?= $alreadyQueued ? 'disabled' : '' ?>>
          <?= $alreadyQueued ? 'Print Requested' : 'Request Print' ?>
        </button>
      </form>
    <?php
}
?>

<?php if ($isSavedView): ?>

  <main class="results" id="main-content">
    <?php
      $backToSearchParams = $_GET;
      unset($backToSearchParams['saved'], $backToSearchParams['view']);
    ?>
    <a class="back-link" href="https://lawlibrary.maxxlms.com/search.php<?= $backToSearchParams !== [] ? '?' . h(http_build_query($backToSearchParams)) : '' ?>">&larr; Back to search</a>
    <h2>My Saved Cases</h2>
    <?php if ($username === null): ?>
      <div class="empty-state"><p>Sign-in info not detected -- open this page from your course to see saved cases.</p></div>
    <?php elseif (empty($savedCases)): ?>
      <div class="empty-state"><p>You haven't saved any cases yet. Use "Save Case" on a search result to add one here.</p></div>
    <?php else: ?>
      <table class="saved-table">
        <thead>
          <tr><th>Case</th><th>Court</th><th>Date filed</th><th>Saved</th><th></th></tr>
        </thead>
        <tbody>
          <?php foreach ($savedCases as $sc): ?>
            <tr>
              <td><?= h($sc['case_name'] ?? 'Untitled opinion') ?></td>
              <td><?= h($sc['court_name'] ?? '') ?></td>
              <td><?= h($sc['date_filed'] ?? '') ?></td>
              <td><?= h(substr((string) $sc['saved_at'], 0, 10)) ?></td>
              <td><a class="view-link" href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(['view' => (string) $sc['opinion_id'], 'username' => $username])) ?>">View</a></td>
            </tr>
          <?php endforeach; ?>
        </tbody>
      </table>
    <?php endif; ?>
  </main>

<?php elseif ($viewId !== null): ?>

  <main class="results" id="main-content">
    <?php
      $backToResultsParams = $_GET;
      unset($backToResultsParams['view']);
    ?>
    <a class="back-link" href="https://lawlibrary.maxxlms.com/search.php<?= $backToResultsParams !== [] ? '?' . h(http_build_query($backToResultsParams)) : '' ?>">&larr; Back to results</a>


    <?php if ($viewDoc === null): ?>
      <div class="error-state">
        <p>That opinion could not be found. It may have been removed or the link may be incorrect.</p>
      </div>
    <?php else: ?>
      <article class="opinion-detail">
        <h2><?= h($viewDoc['case_name'] ?? 'Untitled opinion') ?></h2>
        <div class="result-meta">
          <span><strong>Court:</strong> <?= h($viewDoc['court_name'] ?? 'Unknown court') ?></span>
          <span><strong>Date filed:</strong> <?= h($viewDoc['date_filed'] ?? 'Unknown') ?></span>
          <?php if (!empty($viewDoc['precedential_status'])): ?>
            <span><strong>Status:</strong> <?= h($viewDoc['precedential_status']) ?></span>
          <?php endif; ?>
          <?php if (!empty($viewDoc['docket_number'])): ?>
            <span><strong>Docket:</strong> <?= h($viewDoc['docket_number']) ?></span>
          <?php endif; ?>
        </div>
        <?php if (!empty($viewDoc['citations']) && is_array($viewDoc['citations'])): ?>
          <p><strong>Citations:</strong> <?= h(implode('; ', array_map('strval', $viewDoc['citations']))) ?></p>
        <?php endif; ?>

        <?php if ($username !== null): ?>
          <div class="action-row">
            <?php render_case_actions($username, $viewDoc, $csrfToken); ?>
          </div>
        <?php endif; ?>

        <!-- Opinion text was stripped of all HTML (including any embedded
             links/images) at ingestion time, and is rendered here purely as
             escaped text -- never as raw HTML -- so nothing external can
             ever load from this block. -->
        <div class="opinion-text"><?= h(strip_external_references($viewDoc['text'] ?? $viewDoc['html_lawbox'] ?? null) ?? 'Full text is not available for this opinion.') ?></div>
      </article>
    <?php endif; ?>
  </main>

<?php else: ?>

  <aside class="filters">
    <form method="get" action="https://lawlibrary.maxxlms.com/search.php" id="filter-form">
      <input type="hidden" name="q" value="<?= h($query) ?>">
      <?php if ($username !== null): ?><input type="hidden" name="username" value="<?= h($username) ?>"><?php endif; ?>

      <h2>Filters</h2>

      <fieldset>
        <legend>Court</legend>
        <?php if (empty($facetDistribution['court_name'])): ?>
          <p class="facet-count">No filters available yet.</p>
        <?php else: ?>
          <?php foreach ($facetDistribution['court_name'] as $courtName => $count): ?>
            <div class="filter-option">
              <input
                type="checkbox"
                id="court-<?= h(md5((string) $courtName)) ?>"
                name="courts[]"
                value="<?= h((string) $courtName) ?>"
                <?= is_checked($selectedCourts, (string) $courtName) ? 'checked' : '' ?>
              >
              <label for="court-<?= h(md5((string) $courtName)) ?>">
                <?= h((string) $courtName) ?> <span class="facet-count">(<?= (int) $count ?>)</span>
              </label>
            </div>
          <?php endforeach; ?>
        <?php endif; ?>
      </fieldset>

      <fieldset>
        <legend>Precedential status</legend>
        <?php if (empty($facetDistribution['precedential_status'])): ?>
          <p class="facet-count">No filters available yet.</p>
        <?php else: ?>
          <?php foreach ($facetDistribution['precedential_status'] as $status => $count): ?>
            <div class="filter-option">
              <input
                type="checkbox"
                id="status-<?= h(md5((string) $status)) ?>"
                name="status[]"
                value="<?= h((string) $status) ?>"
                <?= is_checked($selectedStatus, (string) $status) ? 'checked' : '' ?>
              >
              <label for="status-<?= h(md5((string) $status)) ?>">
                <?= h((string) $status) ?> <span class="facet-count">(<?= (int) $count ?>)</span>
              </label>
            </div>
          <?php endforeach; ?>
        <?php endif; ?>
      </fieldset>

      <fieldset>
        <legend>Year filed</legend>
        <div class="year-range">
          <label for="year_from" style="position:absolute; left:-9999px;">From year</label>
          <input type="number" id="year_from" name="year_from" placeholder="From" min="1600" max="<?= $currentYear + 1 ?>"
                 value="<?= $yearFrom > 0 ? (int) $yearFrom : '' ?>">
          <span>&ndash;</span>
          <label for="year_to" style="position:absolute; left:-9999px;">To year</label>
          <input type="number" id="year_to" name="year_to" placeholder="To" min="1600" max="<?= $currentYear + 1 ?>"
                 value="<?= $yearTo > 0 ? (int) $yearTo : '' ?>">
        </div>
      </fieldset>

      <button type="submit" class="apply-btn">Apply filters</button>
      <br>
      <a class="clear-link" href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(array_filter([
          'q' => $query,
          'username' => $username,
      ], static fn($value) => $value !== null && $value !== ''))) ?>">Clear all filters</a>
    </form>
  </aside>

  <main class="results" id="main-content">
    <?php if ($errorMessage !== null): ?>
      <div class="error-state">
        <p><?= h($errorMessage) ?></p>
      </div>
    <?php elseif ($results !== null && count($results) === 0): ?>
      <div class="empty-state">
        <p>No opinions matched your search<?= $query !== '' ? ' for "' . h($query) . '"' : '' ?>.</p>
        <p>Try fewer or different keywords, or clear your filters.</p>
      </div>
    <?php elseif ($results !== null): ?>
      <p class="results-summary">
        <?= number_format($totalHits) ?> result<?= $totalHits === 1 ? '' : 's' ?>
        <?= $query !== '' ? ' for "' . h($query) . '"' : '' ?>
        &mdash; page <?= (int) $page ?> of <?= (int) $totalPages ?>
      </p>

      <?php foreach ($results as $hit): ?>
    <?php 
    $formatted = $hit['_formatted'] ?? []; 
    $caseName = $formatted['case_name'] ?? ($hit['case_name'] ?? 'Untitled opinion'); 
    
    // Look for text or fallback to html_lawbox content
    $rawSnippet = $formatted['text'] ?? $formatted['html_lawbox'] ?? $hit['html_lawbox'] ?? '';
    // Clean up long text so it behaves like a neat snippet
    $snippetHtml = (strlen($rawSnippet) > 300) ? substr(strip_tags($rawSnippet), 0, 300) . '...' : strip_tags($rawSnippet);
    ?> 
    <article class="result-card"> 
        <h3><?= highlighted_text((string) $caseName) ?></h3> 
        <div class="result-meta"> 
            <span><strong>Court:</strong> <?= h($hit['court_name'] ?? 'Unknown court') ?></span> 
            <span><strong>Date filed:</strong> <?= h($hit['date_filed'] ?? 'Unknown') ?></span> 
            <?php if (!empty($hit['precedential_status'])): ?> 
                <span><strong>Status:</strong> <?= h($hit['precedential_status']) ?></span> 
            <?php endif; ?> 
        </div> 
        <?php if ($snippetHtml !== ''): ?> 
            <p class="result-snippet">&hellip;<?= h($snippetHtml) ?>&hellip;</p> 
        <?php endif; ?> 
        <div class="action-row">
          <?php
            $viewParams = $_GET;
            $viewParams['view'] = (string) ($hit['id'] ?? '');
            if ($username !== null) {
                $viewParams['username'] = $username;
            }
          ?>
          <a class="view-link" href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query($viewParams)) ?>">View full opinion</a>
          <?php render_case_actions($username, $hit, $csrfToken); ?>
        </div>
    </article> 
<?php endforeach; ?>


      <?php if ($totalPages > 1): ?>
        <nav class="pagination" aria-label="Search results pages">
          <?php if ($page > 1): ?>
            <a href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(array_merge($_GET, ['page' => $page - 1]))) ?>">&laquo; Prev</a>
          <?php endif; ?>

          <?php
            $windowStart = max(1, $page - 2);
            $windowEnd   = min($totalPages, $page + 2);
          ?>
          <?php if ($windowStart > 1): ?>
            <a href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(array_merge($_GET, ['page' => 1]))) ?>">1</a>
            <?php if ($windowStart > 2): ?><span>&hellip;</span><?php endif; ?>
          <?php endif; ?>

          <?php for ($p = $windowStart; $p <= $windowEnd; $p++): ?>
            <?php if ($p === $page): ?>
              <span class="current" aria-current="page"><?= $p ?></span>
            <?php else: ?>
              <a href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(array_merge($_GET, ['page' => $p]))) ?>"><?= $p ?></a>
            <?php endif; ?>
          <?php endfor; ?>

          <?php if ($windowEnd < $totalPages): ?>
            <?php if ($windowEnd < $totalPages - 1): ?><span>&hellip;</span><?php endif; ?>
            <a href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(array_merge($_GET, ['page' => $totalPages]))) ?>"><?= $totalPages ?></a>
          <?php endif; ?>

          <?php if ($page < $totalPages): ?>
            <a href="https://lawlibrary.maxxlms.com/search.php?<?= h(http_build_query(array_merge($_GET, ['page' => $page + 1]))) ?>">Next &raquo;</a>
          <?php endif; ?>
        </nav>
      <?php endif; ?>

    <?php else: ?>
      <div class="empty-state">
        <p>Enter a search term above, or use the filters to browse opinions.</p>
      </div>
    <?php endif; ?>
  </main>

<?php endif; ?>

</div>

<footer class="site-footer">
  &copy; 2026 MaxxContent LLC. All rights reserved.
</footer>

<script>
  // Progressive enhancement only: auto-submit the filter form when a
  // checkbox changes, so users don't have to hunt for the Apply button.
  // Everything above works fine with this disabled.
  (function () {
    var form = document.getElementById('filter-form');
    if (!form) return;
    var boxes = form.querySelectorAll('input[type="checkbox"]');
    for (var i = 0; i < boxes.length; i++) {
      boxes[i].addEventListener('change', function () {
        form.submit();
      });
    }
  })();
</script>
</body>
</html>
