/**
 * Data builder for the IT Glue asset audit pipeline.
 *
 * Pulls every input the LLM needs to evaluate one IT Glue record (Application
 * flexible asset OR Configuration) against ticket history and IT Glue's own
 * field schema:
 *
 *   1. The asset's current contents (redacted).
 *   2. The field schema with hints (IT Glue's per-field documentation, or
 *      curated hints for Configurations which don't expose a *_fields table).
 *   3. Peer exemplars from the same client — well-filled assets of the same
 *      type, redacted.
 *   4. Best-in-class exemplars across all clients of the same type.
 *   5. Per-field fill-rate stats (per-client + global).
 *   6. Recent ticket fingerprints whose applications/devices overlap the
 *      asset, with summaries.
 *
 * Phase 4.1 additions:
 *   - assetType dispatch: 'flexible_asset' | 'configuration'
 *   - ticketScopeAnalysisId: when set, ticket evidence is the single source
 *     analysis only, so the LLM looks for what *this ticket* taught us.
 */

import postgresClient from '@/lib/services/postgres-client';
import { redact } from '../itglue-redact';
import {
  listLatestEvidenceForAsset,
  listLatestEvidenceForCompany,
} from '@/lib/services/rmm/persistence';

// ─── Types ────────────────────────────────────────────────────────────────

export type AuditAssetType = 'flexible_asset' | 'configuration';

/**
 * Generic asset shape the prompt consumes. For flexible assets, `traits` is
 * the IT Glue traits blob. For configurations, `traits` is a synthesized
 * map of editable columns → current values, so the prompt stays
 * type-agnostic.
 */
export interface AuditAssetRow {
  id: string;
  organization_id: string | null;
  organization_name: string | null;
  type_id: string | null;
  type_name: string | null;
  name: string | null;
  traits: Record<string, unknown>;
}

export interface AuditFieldRow {
  id: string;
  name: string;
  kind: string | null;
  hint: string | null;
  required: boolean;
}

export interface FillRateRow {
  field_name: string;
  fill_rate: number; // 0..1
}

export interface TicketEvidenceRow {
  ticket_number: string;
  triggered_at: string;
  summary: string | null;
  fingerprint: Record<string, unknown>;
}

/**
 * Phase 4.2: live evidence captured by an Overshell execution. The audit
 * pipeline injects this as a 7th LLM context arm.
 */
export interface RmmEvidenceRow {
  execution_id: string;
  script_id: string;
  target_type: 'site_anchor' | 'asset_self';
  target_hostname: string | null;
  captured_at: string;
  parsed: unknown;
}

export interface AssetAuditContext {
  asset_type: AuditAssetType;
  asset: AuditAssetRow;
  type_id: number | null;
  type_name: string | null;
  fields: AuditFieldRow[];
  peer_same_client: AuditAssetRow[];
  peer_global: AuditAssetRow[];
  fill_rate_client: FillRateRow[];
  fill_rate_global: FillRateRow[];
  ticket_evidence: TicketEvidenceRow[];
  /** Set when the audit was launched from a single ticket's analysis. */
  ticket_scope: { analysis_id: string; ticket_number: string } | null;
  rmm_evidence: RmmEvidenceRow[];
}

// ─── Tunables ─────────────────────────────────────────────────────────────

const PEER_SAME_CLIENT_LIMIT = 5;
const PEER_GLOBAL_LIMIT = 3;
const TICKET_EVIDENCE_LIMIT = 20;

// ─── Errors ───────────────────────────────────────────────────────────────

export class AssetAuditNotFoundError extends Error {
  constructor(assetType: AuditAssetType, assetId: string | number) {
    super(`${assetType} ${assetId} not found in mirror`);
    this.name = 'AssetAuditNotFoundError';
  }
}

// ─── Helpers ──────────────────────────────────────────────────────────────

function fillCount(traits: Record<string, unknown> | null | undefined): number {
  if (!traits) return 0;
  let n = 0;
  for (const k of Object.keys(traits)) {
    const v = traits[k];
    if (v === null || v === undefined) continue;
    if (typeof v === 'string' && v.trim().length === 0) continue;
    if (Array.isArray(v) && v.length === 0) continue;
    n += 1;
  }
  return n;
}

// ─── Flexible-asset loaders ───────────────────────────────────────────────

interface RawFlexAsset {
  id: string;
  organization_id: string | null;
  organization_name: string | null;
  flexible_asset_type_id: string;
  flexible_asset_type_name: string | null;
  name: string | null;
  traits: Record<string, unknown> | null;
}

function flexAssetToAudit(r: RawFlexAsset): AuditAssetRow {
  return {
    id: r.id,
    organization_id: r.organization_id,
    organization_name: r.organization_name,
    type_id: r.flexible_asset_type_id,
    type_name: r.flexible_asset_type_name,
    name: r.name,
    traits: r.traits ?? {},
  };
}

async function loadFlexAsset(assetId: string | number): Promise<AuditAssetRow & { _raw: RawFlexAsset }> {
  const res = await postgresClient.query<RawFlexAsset>(
    `SELECT id::text                       AS id,
            organization_id::text          AS organization_id,
            organization_name,
            flexible_asset_type_id::text   AS flexible_asset_type_id,
            flexible_asset_type_name,
            name,
            traits
       FROM itg_flexible_assets
      WHERE id = $1
        AND COALESCE(archived, false) = false
      LIMIT 1`,
    [assetId]
  );
  if (res.rowCount === 0) throw new AssetAuditNotFoundError('flexible_asset', assetId);
  const r = res.rows[0];
  return { ...flexAssetToAudit(r), _raw: r };
}

async function loadFlexFields(typeId: string): Promise<AuditFieldRow[]> {
  const res = await postgresClient.query<AuditFieldRow>(
    `SELECT id::text AS id, name, kind, hint, required
       FROM itg_flexible_asset_fields
      WHERE flexible_asset_type_id = $1
      ORDER BY id`,
    [typeId]
  );
  return res.rows;
}

async function loadFlexPeerSameClient(
  orgId: string | null,
  typeId: string,
  excludeId: string,
  limit: number
): Promise<AuditAssetRow[]> {
  if (!orgId) return [];
  const res = await postgresClient.query<RawFlexAsset>(
    `SELECT id::text                       AS id,
            organization_id::text          AS organization_id,
            organization_name,
            flexible_asset_type_id::text   AS flexible_asset_type_id,
            flexible_asset_type_name,
            name,
            traits
       FROM itg_flexible_assets
      WHERE organization_id = $1
        AND flexible_asset_type_id = $2
        AND id <> $3
        AND COALESCE(archived, false) = false`,
    [orgId, typeId, excludeId]
  );
  const rows = res.rows.map(flexAssetToAudit);
  rows.sort((a, b) => fillCount(b.traits) - fillCount(a.traits));
  return rows.slice(0, limit);
}

async function loadFlexPeerGlobal(
  typeId: string,
  excludeId: string,
  excludeOrgId: string | null,
  limit: number
): Promise<AuditAssetRow[]> {
  const res = await postgresClient.query<RawFlexAsset>(
    `SELECT id::text                       AS id,
            organization_id::text          AS organization_id,
            organization_name,
            flexible_asset_type_id::text   AS flexible_asset_type_id,
            flexible_asset_type_name,
            name,
            traits
       FROM itg_flexible_assets
      WHERE flexible_asset_type_id = $1
        AND id <> $2
        AND ($3::text IS NULL OR organization_id::text <> $3)
        AND COALESCE(archived, false) = false
      ORDER BY synced_at DESC
      LIMIT 200`,
    [typeId, excludeId, excludeOrgId]
  );
  const rows = res.rows.map(flexAssetToAudit);
  rows.sort((a, b) => fillCount(b.traits) - fillCount(a.traits));
  return rows.slice(0, limit);
}

function flexFieldKey(name: string): string {
  return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}

async function loadFlexFillRates(
  typeId: string,
  orgIdFilter: string | null,
  fields: AuditFieldRow[]
): Promise<FillRateRow[]> {
  const params: unknown[] = [typeId];
  let where = `flexible_asset_type_id = $1 AND COALESCE(archived, false) = false`;
  if (orgIdFilter) {
    params.push(orgIdFilter);
    where += ` AND organization_id = $${params.length}`;
  }
  const res = await postgresClient.query<{ traits: Record<string, unknown> | null }>(
    `SELECT traits FROM itg_flexible_assets WHERE ${where}`,
    params
  );
  const total = res.rowCount ?? 0;
  if (total === 0) return fields.map((f) => ({ field_name: f.name, fill_rate: 0 }));
  return fields.map((f) => {
    const key = flexFieldKey(f.name);
    let filled = 0;
    for (const row of res.rows) {
      const v = row.traits?.[key];
      if (v === null || v === undefined) continue;
      if (typeof v === 'string' && v.trim().length === 0) continue;
      if (Array.isArray(v) && v.length === 0) continue;
      if (
        typeof v === 'object' &&
        v !== null &&
        'values' in (v as Record<string, unknown>) &&
        Array.isArray((v as { values: unknown[] }).values) &&
        (v as { values: unknown[] }).values.length === 0
      ) {
        continue;
      }
      filled += 1;
    }
    return {
      field_name: f.name,
      fill_rate: Math.round((filled / total) * 100) / 100,
    };
  });
}

// ─── Configuration loaders ────────────────────────────────────────────────

interface RawConfiguration {
  id: string;
  organization_id: string | null;
  organization_name: string | null;
  configuration_type_id: string | null;
  configuration_type_name: string | null;
  configuration_status_id: string | null;
  configuration_status_name: string | null;
  manufacturer_id: string | null;
  manufacturer_name: string | null;
  model_id: string | null;
  model_name: string | null;
  operating_system_id: string | null;
  operating_system_name: string | null;
  contact_id: string | null;
  location_id: string | null;
  name: string;
  hostname: string | null;
  primary_ip: string | null;
  mac_address: string | null;
  serial_number: string | null;
  asset_tag: string | null;
  position: string | null;
  notes: string | null;
  operating_system_notes: string | null;
}

const CONFIGURATION_FIELDS: AuditFieldRow[] = [
  { id: '1', name: 'name', kind: 'Text', hint: 'Display name. Match the hostname or a stable label techs recognize.', required: true },
  { id: '2', name: 'hostname', kind: 'Text', hint: 'FQDN or NetBIOS name as it appears on the network. Should match what shows up in DNS and on the asset itself.', required: false },
  { id: '3', name: 'primary_ip', kind: 'Text', hint: 'Primary IP address. Static where possible; capture even if DHCP-assigned for current state.', required: false },
  { id: '4', name: 'mac_address', kind: 'Text', hint: 'Primary network adapter MAC.', required: false },
  { id: '5', name: 'serial_number', kind: 'Text', hint: 'Hardware serial / VM UUID — needed for vendor warranty calls.', required: false },
  { id: '6', name: 'asset_tag', kind: 'Text', hint: 'Physical or logical asset tag if the customer uses one.', required: false },
  { id: '7', name: 'position', kind: 'Text', hint: 'Rack U position, room location, or VM cluster placement.', required: false },
  { id: '8', name: 'configuration_type_name', kind: 'Select', hint: 'Server, Workstation, Printer, Firewall, Switch, etc. Drives downstream filtering and audit prompts.', required: false },
  { id: '9', name: 'configuration_status_name', kind: 'Select', hint: 'Active, Inactive, Decommissioned, Spare. Stale Active records hide retired infra.', required: false },
  { id: '10', name: 'manufacturer_name', kind: 'Tag', hint: 'Hardware/VM platform vendor (Dell, HPE, VMware, Hyper-V).', required: false },
  { id: '11', name: 'model_name', kind: 'Tag', hint: 'Model identifier (PowerEdge R650, Surface Laptop 5).', required: false },
  { id: '12', name: 'operating_system_name', kind: 'Tag', hint: 'OS + version (Windows Server 2019, Ubuntu 22.04). Stale OS = stale patch picture.', required: false },
  { id: '13', name: 'operating_system_notes', kind: 'Textbox', hint: 'OS-specific gotchas: hotfix levels, schedule windows, named services and what they do, important roles installed (DC, DHCP, DNS, RDS).', required: false },
  { id: '14', name: 'notes', kind: 'Textbox', hint: 'Free-text general notes. Keep architectural details (integration paths, data flows) here only if no structured field fits.', required: false },
  { id: '15', name: 'contact_id', kind: 'Tag', hint: 'Primary user / responsible contact (workstation = end user; server = champion).', required: false },
  { id: '16', name: 'location_id', kind: 'Tag', hint: 'IT Glue location/site this configuration lives at.', required: false },
];

function configToAudit(r: RawConfiguration): AuditAssetRow {
  // Synthesize a traits-style map for prompt consistency.
  const traits: Record<string, unknown> = {
    name: r.name ?? null,
    hostname: r.hostname ?? null,
    primary_ip: r.primary_ip ?? null,
    mac_address: r.mac_address ?? null,
    serial_number: r.serial_number ?? null,
    asset_tag: r.asset_tag ?? null,
    position: r.position ?? null,
    configuration_type_name: r.configuration_type_name ?? null,
    configuration_status_name: r.configuration_status_name ?? null,
    manufacturer_name: r.manufacturer_name ?? null,
    model_name: r.model_name ?? null,
    operating_system_name: r.operating_system_name ?? null,
    operating_system_notes: r.operating_system_notes ?? null,
    notes: r.notes ?? null,
    contact_id: r.contact_id ?? null,
    location_id: r.location_id ?? null,
  };
  return {
    id: r.id,
    organization_id: r.organization_id,
    organization_name: r.organization_name,
    type_id: r.configuration_type_id,
    type_name: r.configuration_type_name,
    name: r.name,
    traits,
  };
}

const CONFIG_SELECT = `
  id::text                          AS id,
  organization_id::text             AS organization_id,
  organization_name,
  configuration_type_id::text       AS configuration_type_id,
  configuration_type_name,
  configuration_status_id::text     AS configuration_status_id,
  configuration_status_name,
  manufacturer_id::text             AS manufacturer_id,
  manufacturer_name,
  model_id::text                    AS model_id,
  model_name,
  operating_system_id::text         AS operating_system_id,
  operating_system_name,
  contact_id::text                  AS contact_id,
  location_id::text                 AS location_id,
  name, hostname, primary_ip, mac_address, serial_number, asset_tag,
  position, notes, operating_system_notes
`;

async function loadConfiguration(assetId: string | number): Promise<AuditAssetRow & { type_id_str: string | null }> {
  const res = await postgresClient.query<RawConfiguration>(
    `SELECT ${CONFIG_SELECT} FROM itg_configurations WHERE id = $1 LIMIT 1`,
    [assetId]
  );
  if (res.rowCount === 0) throw new AssetAuditNotFoundError('configuration', assetId);
  return { ...configToAudit(res.rows[0]), type_id_str: res.rows[0].configuration_type_id };
}

async function loadConfigPeerSameClient(
  orgId: string | null,
  typeId: string | null,
  excludeId: string,
  limit: number
): Promise<AuditAssetRow[]> {
  if (!orgId || !typeId) return [];
  const res = await postgresClient.query<RawConfiguration>(
    `SELECT ${CONFIG_SELECT}
       FROM itg_configurations
      WHERE organization_id = $1
        AND configuration_type_id = $2
        AND id <> $3`,
    [orgId, typeId, excludeId]
  );
  const rows = res.rows.map(configToAudit);
  rows.sort((a, b) => fillCount(b.traits) - fillCount(a.traits));
  return rows.slice(0, limit);
}

async function loadConfigPeerGlobal(
  typeId: string | null,
  excludeId: string,
  excludeOrgId: string | null,
  limit: number
): Promise<AuditAssetRow[]> {
  if (!typeId) return [];
  const res = await postgresClient.query<RawConfiguration>(
    `SELECT ${CONFIG_SELECT}
       FROM itg_configurations
      WHERE configuration_type_id = $1
        AND id <> $2
        AND ($3::text IS NULL OR organization_id::text <> $3)
      LIMIT 200`,
    [typeId, excludeId, excludeOrgId]
  );
  const rows = res.rows.map(configToAudit);
  rows.sort((a, b) => fillCount(b.traits) - fillCount(a.traits));
  return rows.slice(0, limit);
}

async function loadConfigFillRates(
  typeId: string | null,
  orgIdFilter: string | null
): Promise<FillRateRow[]> {
  if (!typeId) {
    return CONFIGURATION_FIELDS.map((f) => ({ field_name: f.name, fill_rate: 0 }));
  }
  const params: unknown[] = [typeId];
  let where = `configuration_type_id = $1`;
  if (orgIdFilter) {
    params.push(orgIdFilter);
    where += ` AND organization_id = $${params.length}`;
  }
  const res = await postgresClient.query<RawConfiguration>(
    `SELECT ${CONFIG_SELECT} FROM itg_configurations WHERE ${where}`,
    params
  );
  const total = res.rowCount ?? 0;
  if (total === 0) {
    return CONFIGURATION_FIELDS.map((f) => ({ field_name: f.name, fill_rate: 0 }));
  }
  return CONFIGURATION_FIELDS.map((f) => {
    let filled = 0;
    for (const row of res.rows) {
      const v = (row as unknown as Record<string, unknown>)[f.name];
      if (v === null || v === undefined) continue;
      if (typeof v === 'string' && v.trim().length === 0) continue;
      filled += 1;
    }
    return {
      field_name: f.name,
      fill_rate: Math.round((filled / total) * 100) / 100,
    };
  });
}

// ─── Ticket evidence (shared) ────────────────────────────────────────────

async function loadTicketEvidence(
  orgId: string | null,
  assetName: string | null,
  limit: number
): Promise<TicketEvidenceRow[]> {
  if (!orgId || !assetName) return [];
  const compRes = await postgresClient.query<{ id: string }>(
    `SELECT c.id::text AS id
       FROM companies c
       JOIN itg_organizations o ON LOWER(c.company_name) = LOWER(o.name)
      WHERE o.id = $1
      LIMIT 1`,
    [orgId]
  );
  if (compRes.rowCount === 0) return [];
  const companyId = compRes.rows[0].id;
  const lowerName = assetName.toLowerCase();
  const res = await postgresClient.query<{
    ticket_number: string;
    triggered_at: Date;
    summary: string | null;
    fingerprint: Record<string, unknown> | null;
  }>(
    `SELECT aa.ticket_number, aa.triggered_at, aa.summary,
            aa.aggregate_fingerprint AS fingerprint
       FROM analyzer_analyses aa
       JOIN tickets t ON t.ticket_number = aa.ticket_number
      WHERE t.company_id = $1
        AND aa.status = 'complete'
        AND aa.aggregate_fingerprint IS NOT NULL
        AND (
          LOWER(COALESCE(aa.summary, '')) LIKE '%' || $2 || '%'
          OR aa.aggregate_fingerprint::text ILIKE '%' || $2 || '%'
        )
      ORDER BY aa.triggered_at DESC
      LIMIT $3`,
    [companyId, lowerName, limit]
  );
  return res.rows.map((r) => ({
    ticket_number: r.ticket_number,
    triggered_at: r.triggered_at.toISOString(),
    summary: r.summary,
    fingerprint: r.fingerprint ?? {},
  }));
}

/**
 * Ticket-scoped evidence — returns just the one analysis the audit was
 * launched from. Used when the user clicks "Check IT Glue documentation"
 * on a specific analysis page; the audit looks for what *this ticket*
 * taught us, not all-time history.
 */
/**
 * Phase 4.2: pull the latest successful Overshell evidence for the audit's
 * client + asset. Site-anchored scripts: latest per script_id within 7
 * days. Asset-self scripts: latest per script_id, no time limit
 * (asset-self facts are durable until something changes).
 */
async function loadRmmEvidence(
  itglueOrgId: string | null,
  assetType: AuditAssetType,
  assetId: string
): Promise<RmmEvidenceRow[]> {
  if (!itglueOrgId) return [];
  // Map IT Glue org → Autotask company (same join the ticket-evidence loader uses).
  const compRes = await postgresClient.query<{ id: string }>(
    `SELECT c.id::text AS id
       FROM companies c
       JOIN itg_organizations o ON LOWER(c.company_name) = LOWER(o.name)
      WHERE o.id = $1
      LIMIT 1`,
    [itglueOrgId]
  );
  if (compRes.rowCount === 0) return [];
  const companyId = compRes.rows[0].id;

  const [siteAnchored, assetSelf] = await Promise.all([
    listLatestEvidenceForCompany(companyId, 7),
    listLatestEvidenceForAsset(assetType, assetId),
  ]);

  // Combine: site-anchored first (general context), then asset-self.
  const out: RmmEvidenceRow[] = [];
  for (const e of siteAnchored) {
    if (e.targetType !== 'site_anchor') continue;
    out.push({
      execution_id: e.id,
      script_id: e.scriptId,
      target_type: e.targetType,
      target_hostname: e.targetHostname,
      captured_at: e.completedAt ?? e.queuedAt,
      parsed: redact(e.parsedEvidence ?? null),
    });
  }
  for (const e of assetSelf) {
    out.push({
      execution_id: e.id,
      script_id: e.scriptId,
      target_type: e.targetType,
      target_hostname: e.targetHostname,
      captured_at: e.completedAt ?? e.queuedAt,
      parsed: redact(e.parsedEvidence ?? null),
    });
  }
  return out;
}

async function loadSingleTicketEvidence(
  analysisId: string
): Promise<TicketEvidenceRow[]> {
  const res = await postgresClient.query<{
    ticket_number: string;
    triggered_at: Date;
    summary: string | null;
    fingerprint: Record<string, unknown> | null;
  }>(
    `SELECT ticket_number, triggered_at, summary,
            aggregate_fingerprint AS fingerprint
       FROM analyzer_analyses
      WHERE id = $1 AND status = 'complete'
      LIMIT 1`,
    [analysisId]
  );
  if (res.rowCount === 0) return [];
  const r = res.rows[0];
  return [
    {
      ticket_number: r.ticket_number,
      triggered_at: r.triggered_at.toISOString(),
      summary: r.summary,
      fingerprint: r.fingerprint ?? {},
    },
  ];
}

// ─── Top-level builder ────────────────────────────────────────────────────

export interface BuildAssetAuditContextInput {
  assetType: AuditAssetType;
  assetId: string | number;
  /** When set, ticket evidence is just this one analysis (ticket-first mode). */
  ticketScopeAnalysisId?: string;
}

export async function buildAssetAuditContext(
  input: BuildAssetAuditContextInput
): Promise<AssetAuditContext> {
  const { assetType, assetId, ticketScopeAnalysisId } = input;

  if (assetType === 'flexible_asset') {
    const asset = await loadFlexAsset(assetId);
    const typeId = asset.type_id ?? '';
    const fields = await loadFlexFields(typeId);

    const [peerSameClient, peerGlobal, fillRateClient, fillRateGlobal, rmmEvidence] =
      await Promise.all([
        loadFlexPeerSameClient(asset.organization_id, typeId, asset.id, PEER_SAME_CLIENT_LIMIT),
        loadFlexPeerGlobal(typeId, asset.id, asset.organization_id, PEER_GLOBAL_LIMIT),
        loadFlexFillRates(typeId, asset.organization_id, fields),
        loadFlexFillRates(typeId, null, fields),
        loadRmmEvidence(asset.organization_id, 'flexible_asset', asset.id),
      ]);

    let ticketEvidence: TicketEvidenceRow[];
    let ticketScope: AssetAuditContext['ticket_scope'] = null;
    if (ticketScopeAnalysisId) {
      ticketEvidence = await loadSingleTicketEvidence(ticketScopeAnalysisId);
      if (ticketEvidence.length > 0) {
        ticketScope = {
          analysis_id: ticketScopeAnalysisId,
          ticket_number: ticketEvidence[0].ticket_number,
        };
      }
    } else {
      ticketEvidence = await loadTicketEvidence(asset.organization_id, asset.name, TICKET_EVIDENCE_LIMIT);
    }

    return {
      asset_type: 'flexible_asset',
      asset: { ...asset, traits: redact(asset.traits) },
      type_id: Number(typeId) || null,
      type_name: asset.type_name,
      fields,
      peer_same_client: peerSameClient.map((p) => ({ ...p, traits: redact(p.traits) })),
      peer_global: peerGlobal.map((p) => ({ ...p, traits: redact(p.traits) })),
      fill_rate_client: fillRateClient,
      fill_rate_global: fillRateGlobal,
      ticket_evidence: ticketEvidence,
      ticket_scope: ticketScope,
      rmm_evidence: rmmEvidence,
    };
  }

  // Configurations
  const config = await loadConfiguration(assetId);
  const typeId = config.type_id_str;

  const [peerSameClient, peerGlobal, fillRateClient, fillRateGlobal, rmmEvidence] =
    await Promise.all([
      loadConfigPeerSameClient(config.organization_id, typeId, config.id, PEER_SAME_CLIENT_LIMIT),
      loadConfigPeerGlobal(typeId, config.id, config.organization_id, PEER_GLOBAL_LIMIT),
      loadConfigFillRates(typeId, config.organization_id),
      loadConfigFillRates(typeId, null),
      loadRmmEvidence(config.organization_id, 'configuration', config.id),
    ]);

  let ticketEvidence: TicketEvidenceRow[];
  let ticketScope: AssetAuditContext['ticket_scope'] = null;
  if (ticketScopeAnalysisId) {
    ticketEvidence = await loadSingleTicketEvidence(ticketScopeAnalysisId);
    if (ticketEvidence.length > 0) {
      ticketScope = {
        analysis_id: ticketScopeAnalysisId,
        ticket_number: ticketEvidence[0].ticket_number,
      };
    }
  } else {
    // For configurations, also try matching on hostname for richer evidence.
    const matchTerm = config.name ?? (config.traits.hostname as string | null) ?? null;
    ticketEvidence = await loadTicketEvidence(config.organization_id, matchTerm, TICKET_EVIDENCE_LIMIT);
  }

  return {
    asset_type: 'configuration',
    asset: { ...config, traits: redact(config.traits) },
    type_id: typeId !== null ? Number(typeId) : null,
    type_name: config.type_name,
    fields: CONFIGURATION_FIELDS,
    peer_same_client: peerSameClient.map((p) => ({ ...p, traits: redact(p.traits) })),
    peer_global: peerGlobal.map((p) => ({ ...p, traits: redact(p.traits) })),
    fill_rate_client: fillRateClient,
    fill_rate_global: fillRateGlobal,
    ticket_evidence: ticketEvidence,
    ticket_scope: ticketScope,
    rmm_evidence: rmmEvidence,
  };
}

// Exported for tests + observability.
export const _ASSET_AUDIT_INTERNALS = {
  PEER_SAME_CLIENT_LIMIT,
  PEER_GLOBAL_LIMIT,
  TICKET_EVIDENCE_LIMIT,
  CONFIGURATION_FIELDS,
  fillCount,
};
