/**
 * IT Glue search facade for the AI Ticket Analyzer.
 *
 * SECURITY-CRITICAL. Every doc this returns has been run through `redact()`
 * before leaving the function. Callers must treat the return value as the
 * only thing that may flow into LLM context, log lines, or analyzer_analyses
 * rows. The non-redacting `itglue-client.ts` is for non-LLM use only — never
 * import its results directly into the analyzer pipeline.
 *
 * Spec: docs/wulf-pulse-ticket-analyzer-prompt.md → "IT Glue client" + "Stage 2 — IT Glue Retrieval"
 */

import { getITGlueClient } from '@/lib/services/itglue-client';
import { redact } from './itglue-redact';
import aliases from './itglue-aliases.json';

const MAX_DOCS_RETURNED = 10;
const PER_DOC_BODY_CHAR_CAP = 2_000;

export interface RedactedDoc {
  id: string;
  name: string;
  doc_type: 'configuration' | 'flexible_asset' | 'document';
  organization_id: string;
  organization_name: string;
  /** Capped at PER_DOC_BODY_CHAR_CAP; redacted of any sensitive fields. */
  snippet: string;
  /** External link to the doc in IT Glue, if available. */
  url: string | null;
  /** The IT Glue updated-at timestamp, useful for ranking. */
  updated_at: string | null;
}

export interface ITGlueSearchInput {
  /** Autotask company name from the ticket. Normalized + alias-resolved internally. */
  org_name: string;
  /** Search hints from Stage 1 triage (e.g. ["AMS360 App Access Key", "VSSO admin"]). */
  hints: string[];
}

export interface ITGlueSearchResult {
  /** Resolved IT Glue org id, or null if no match. */
  org_id: string | null;
  /** Resolved IT Glue org name, or the input as-is if not resolved. */
  org_name: string;
  docs: RedactedDoc[];
  /** Whether the org-name → org-id resolution came from the alias map. */
  alias_used: boolean;
}

function normalize(name: string): string {
  return name.trim().toLowerCase().replace(/\s+/g, ' ');
}

/**
 * Look up an IT Glue org id by Autotask company name.
 *   1. Check `itglue-aliases.json` for an exact normalized match.
 *   2. Otherwise, query IT Glue for orgs whose name matches.
 * Returns null if no match.
 */
export async function resolveOrgId(
  orgName: string
): Promise<{ org_id: string | null; org_name: string; alias_used: boolean }> {
  const key = normalize(orgName);
  const aliasMap = aliases as Record<string, string>;
  const aliasHit = aliasMap[key];
  if (aliasHit && !key.startsWith('_')) {
    return { org_id: aliasHit, org_name: orgName, alias_used: true };
  }

  // Fall through to live IT Glue lookup. We tolerate failures here — the
  // analyzer should still run without IT Glue context if the lookup errors.
  try {
    const client = getITGlueClient();
    const orgs = await client.getOrganizations({ name: orgName });
    if (orgs.length === 0) return { org_id: null, org_name: orgName, alias_used: false };
    const exact = orgs.find((o) => normalize(o.name) === key);
    const chosen = exact ?? orgs[0];
    return { org_id: String(chosen.id), org_name: chosen.name, alias_used: false };
  } catch (err) {
    console.warn(
      `[itglue-search] org lookup failed for ${JSON.stringify(orgName)}: ${err instanceof Error ? err.message : String(err)}`
    );
    return { org_id: null, org_name: orgName, alias_used: false };
  }
}

/**
 * Trim a string to the configured cap. Adds an ellipsis marker so the LLM
 * knows the doc was truncated rather than naturally short.
 */
function cap(text: string): string {
  if (text.length <= PER_DOC_BODY_CHAR_CAP) return text;
  return text.slice(0, PER_DOC_BODY_CHAR_CAP - 20) + '… [truncated]';
}

/**
 * Run a search and return at most MAX_DOCS_RETURNED redacted docs. Every doc
 * body is capped + redacted before it is returned. The non-redacted IT Glue
 * payload never escapes this function.
 */
export async function itglueSearch(
  input: ITGlueSearchInput
): Promise<ITGlueSearchResult> {
  const resolved = await resolveOrgId(input.org_name);
  if (!resolved.org_id) {
    return {
      org_id: null,
      org_name: resolved.org_name,
      docs: [],
      alias_used: resolved.alias_used,
    };
  }

  const client = getITGlueClient();
  const docs: RedactedDoc[] = [];
  const seen = new Set<string>();

  // Configurations — usually the most directly applicable type for ticket context.
  try {
    const configs = await client.getConfigurations({ organizationId: resolved.org_id });
    for (const c of configs) {
      if (docs.length >= MAX_DOCS_RETURNED) break;
      const dedupeKey = `cfg:${c.id}`;
      if (seen.has(dedupeKey)) continue;
      seen.add(dedupeKey);
      const redacted = redact({
        name: c.name,
        hostname: c.hostname,
        primary_ip: c.primaryIp,
        configuration_type: c.configurationTypeName,
        manufacturer: c.manufacturerName,
        model: c.modelName,
        os: c.operatingSystemName,
        notes: c.notes ?? '',
      });
      docs.push({
        id: String(c.id),
        name: c.name,
        doc_type: 'configuration',
        organization_id: resolved.org_id,
        organization_name: resolved.org_name,
        snippet: cap(JSON.stringify(redacted)),
        url: null,
        updated_at: c.updatedAt ?? null,
      });
    }
  } catch (err) {
    console.warn(
      `[itglue-search] configurations fetch failed: ${err instanceof Error ? err.message : String(err)}`
    );
  }

  // Flexible assets — runbooks, integrations, app-specific docs.
  try {
    const flex = await client.getFlexibleAssetsForOrganization(resolved.org_id);
    for (const a of flex) {
      if (docs.length >= MAX_DOCS_RETURNED) break;
      const dedupeKey = `flex:${a.id}`;
      if (seen.has(dedupeKey)) continue;
      seen.add(dedupeKey);
      const redacted = redact({
        name: a.name,
        type: a.flexibleAssetTypeName,
        traits: a.traits, // any trait keyed like 'password'/'api-key'/etc. is redacted by `redact()`
      });
      docs.push({
        id: String(a.id),
        name: a.name,
        doc_type: 'flexible_asset',
        organization_id: resolved.org_id,
        organization_name: resolved.org_name,
        snippet: cap(JSON.stringify(redacted)),
        url: null,
        updated_at: a.updatedAt ?? null,
      });
    }
  } catch (err) {
    console.warn(
      `[itglue-search] flexible_assets fetch failed: ${err instanceof Error ? err.message : String(err)}`
    );
  }

  // Hint-based filtering: if hints are provided, prefer docs whose name or
  // type matches one of the hints (case-insensitive substring). Falls back to
  // the unsorted result if no hints overlap.
  if (input.hints.length > 0) {
    const hintsLower = input.hints.map((h) => h.toLowerCase());
    const matchScore = (d: RedactedDoc) => {
      const haystack = `${d.name} ${d.doc_type}`.toLowerCase();
      return hintsLower.reduce((acc, h) => acc + (haystack.includes(h) ? 1 : 0), 0);
    };
    docs.sort((a, b) => matchScore(b) - matchScore(a));
  }

  return {
    org_id: resolved.org_id,
    org_name: resolved.org_name,
    docs: docs.slice(0, MAX_DOCS_RETURNED),
    alias_used: resolved.alias_used,
  };
}

// Test-only constants.
export const _ITGLUE_SEARCH_INTERNALS = {
  MAX_DOCS_RETURNED,
  PER_DOC_BODY_CHAR_CAP,
};
