/**
 * SB-S3c — the cross-silo assembler (the payoff).
 *
 * Answers the arc's north-star query (`architecture.md:17`): *which raw material
 * actually performs?* — `specific → post → measured analytics`. The post record
 * (`ingest/published/<contentId>.md`) already carries the `specifics`/`trends` ids
 * it was built from (SB-S3c hub-side threading); this module joins each post to
 * its measured analytics row and surfaces the whole graph.
 *
 * PURE core: `assemblePostGraph({records, analytics})` takes already-loaded inputs
 * and returns the graph — no FS/clock/network. The analytics↔post join is an honest
 * HEURISTIC, never a guaranteed key: analytics carries no body and no URN (only a
 * title-prefix + date), so `matchRow` joins by normalized title-prefix + date with
 * explicit confidence tiers (`high`/`low`/`none`) — a real-CSV `none` is a
 * normalization-tightening signal, not a proof of no match.
 *
 * DECOUPLED: this module treats tributary ids as opaque 12-hex strings and takes a
 * minimal `AnalyticsRowInput` shape — it never imports the analytics/trends/
 * specifics packages. The thin read-only `loadAnalyticsRows` IO inlines a raw-JSON
 * read of the shared data-root (NOT the analytics package's `loadAllPosts`).
 */

import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";

import { dataRoot } from "./dataRoot.js";
import type { PublishedRecord } from "./ingest.js";

/**
 * The minimal analytics-row shape the resolver needs, extracted from the raw
 * `AnalyticsBatch.posts[]` JSON (`analytics/src/models/types.ts`). Note the field
 * is `publishedDate` (analytics) vs `published_date` (the brain record).
 */
export interface AnalyticsRowInput {
  title: string;
  publishedDate: string; // YYYY-MM-DD
  metrics?: { engagementRate?: number } & Record<string, unknown>;
}

export type MatchConfidence = "high" | "low" | "none";

/** A post's matched analytics: the WHOLE row reference (FIX 4), or none. */
export interface PostMatch {
  confidence: MatchConfidence;
  row?: AnalyticsRowInput;
}

export interface PostGraphNode {
  contentId: string;
  published_date: string;
  specifics: string[];
  trends: string[];
  match: PostMatch;
}

/**
 * Minimum normalized-title length to attempt a prefix match. The hook quality-rule
 * floor is 110 chars; 24 normalized chars (~3–5 words) is the shortest opener
 * specific enough that a prefix-match is not coincidental, while staying well under
 * any real hook. Below floor → `none` (an operator can still eyeball).
 */
const PREFIX_FLOOR = 24;

/** Brain-local copy of the specifics-bank `normalizeContent` idiom (NOT imported). */
export function normalize(s: string): string {
  return s.trim().toLowerCase().replace(/\s+/g, " ");
}

/** Strip a trailing LinkedIn truncation marker (`…`/`...`) so a `…`-suffixed export title still prefix-matches. */
function stripTrailingEllipsis(s: string): string {
  return s.replace(/(?:…|\.{3})\s*$/, "").trimEnd();
}

/**
 * Match one analytics row to one published record. Returns the tiered match, or
 * `null` when the row does not qualify (no prefix / below floor) — STUB until S3c
 * Step 3.
 */
export function matchRow(record: PublishedRecord, row: AnalyticsRowInput): PostMatch | null {
  const nt = stripTrailingEllipsis(normalize(row.title));
  if (nt.length < PREFIX_FLOOR) return null; // too short to discriminate → none
  if (!normalize(record.body).startsWith(nt)) return null; // no prefix → none
  const confidence: MatchConfidence = record.published_date === row.publishedDate ? "high" : "low";
  return { confidence, row };
}

/**
 * Assemble the post → raw-material → performance graph. Pure (no FS/clock/network).
 * For each record, the BEST qualifying analytics row: `high` (same date) beats `low`
 * (different date); within a tier, the longest matched title wins. The analytics rows
 * are sorted once (publishedDate desc, title asc) so an exact-length tie is stable —
 * never `readdirSync`-order-dependent.
 */
export function assemblePostGraph(args: {
  records: PublishedRecord[];
  analytics: AnalyticsRowInput[];
}): PostGraphNode[] {
  const analytics = [...args.analytics].sort(
    (a, b) => b.publishedDate.localeCompare(a.publishedDate) || a.title.localeCompare(b.title),
  );
  return args.records.map((record) => {
    let best: PostMatch | null = null;
    let bestLen = -1;
    for (const row of analytics) {
      const m = matchRow(record, row);
      if (!m) continue;
      const len = stripTrailingEllipsis(normalize(row.title)).length;
      const better =
        best === null ||
        (m.confidence === "high" && best.confidence === "low") ||
        (m.confidence === best.confidence && len > bestLen);
      if (better) {
        best = m;
        bestLen = len;
      }
    }
    return {
      contentId: record.id,
      published_date: record.published_date,
      specifics: record.specifics,
      trends: record.trends,
      match: best ?? { confidence: "none" },
    };
  });
}

/**
 * Read-only loader: inline a raw-JSON read of the analytics batches under the shared
 * data-root and extract the minimal row shape. STUB until S3c Step 3.
 *
 * NOTE (root-skew caveat): resolves via the brain `dataRoot` (`${LINKEDIN_STUDIO_DATA}/
 * analytics/posts`); the analytics package additionally honours the deprecated
 * `ANALYTICS_ROOT` override, which this path does NOT — if set to a non-default
 * path, the join degrades to every-post-`none` (accepted cost of the no-import
 * decoupling; the M0 default leaves `ANALYTICS_ROOT` unset).
 */
export function loadAnalyticsRows(): AnalyticsRowInput[] {
  const dir = dataRoot(join("analytics", "posts"));
  if (!existsSync(dir)) return []; // fresh-clone / no imports yet → no rows
  const rows: AnalyticsRowInput[] = [];
  for (const name of readdirSync(dir)) {
    if (!name.endsWith(".json") || name.startsWith(".")) continue;
    try {
      const batch = JSON.parse(readFileSync(join(dir, name), "utf8")) as { posts?: unknown[] };
      for (const p of batch?.posts ?? []) {
        const row = p as Partial<AnalyticsRowInput>;
        if (typeof row?.title === "string" && typeof row?.publishedDate === "string") {
          rows.push({ title: row.title, publishedDate: row.publishedDate, metrics: row.metrics });
        }
      }
    } catch {
      // skip a malformed/unreadable batch file — never crash (mirrors listPublished)
    }
  }
  return rows;
}
