/**
 * Triage-note service (Phase 21, NOTE-01).
 *
 * Gathers a campaign's CURRENT evidence — linked reports/tickets, extracted
 * url indicators (Phase 16), most-recent classification (Phase 19), current
 * remediation state (Phase 20), and a fresh blast-radius lookup (Phase 17) —
 * renders it through Plan 01's `formatTriageNote()` (already sanitized), and
 * posts one internal (non-portal) Autotask `TicketNotes` entry per linked
 * ticket via the existing safe write path (`workflow-engine.ts`'s
 * `createEntity('TicketNotes', ...)` precedent).
 *
 * D-05/D-06: each ticket's write is attempted in its own try/catch INSIDE the
 * loop — one ticket's Autotask failure never aborts the remaining writes, and
 * the generated note text is always returned regardless of write outcome.
 */

import postgresClient from './postgres-client';
import { getAutotaskClient } from './autotask-factory';
import { getBlastRadius, type BlastRadiusResult } from './mimecast-blast-radius';
import { formatTriageNote, type TriageNoteEvidence } from './triage-note-format';

export interface TriageNotePostResult {
  ticketId: string;
  posted: boolean;
  error?: string;
}

export interface TriageNoteResult {
  noteText: string;
  tickets: TriageNotePostResult[];
}

interface ReportRow {
  id: string;
  ticket_id: string;
  ticket_number: string | null;
  title: string | null;
  company_name: string | null;
  requester_contact_id: number | null;
  evidence: unknown;
  created_at: string;
}

interface ClassificationRow {
  verdict: string | null;
  confidence: number | string | null;
  summary: string | null;
  reasons: string[] | string | null;
  recommended_actions: string[] | string | null;
  requires_approval: boolean | null;
  created_at: string;
}

interface RemediationRow {
  action_type: string;
  status: string;
  approved_by: string | null;
  approved_at: string | null;
}

interface IndicatorUrlRow {
  value: string;
}

/** Normalizes a JSONB array column into a string[] regardless of driver JSON parsing (mirrors remediation-service.ts's parseRecommendedActions idiom). */
function parseJsonArray(value: string[] | string | null | undefined): string[] {
  if (Array.isArray(value)) return value;
  if (typeof value === 'string') {
    try {
      const parsed = JSON.parse(value);
      return Array.isArray(parsed) ? parsed : [];
    } catch {
      return [];
    }
  }
  return [];
}

/**
 * Gathers current campaign evidence, renders the sanitized triage note, and
 * posts it as an internal TicketNote to every ticket linked to the campaign
 * (D-01). Always returns the note text (D-06) — a per-ticket write failure is
 * captured on that ticket's result entry without aborting the loop (D-05).
 */
export async function generateAndPostTriageNote(campaignId: string): Promise<TriageNoteResult> {
  const reportsRes = await postgresClient.query<ReportRow>(
    `SELECT id::text, ticket_id::text AS ticket_id, ticket_number, title, company_name,
            requester_contact_id, evidence, created_at::text AS created_at
       FROM reports WHERE campaign_id = $1 ORDER BY created_at ASC`,
    [campaignId]
  );
  const reports = reportsRes.rows;

  const classificationRes = await postgresClient.query<ClassificationRow>(
    `SELECT verdict, confidence::float8 AS confidence, summary, reasons, recommended_actions,
            requires_approval, created_at::text AS created_at
       FROM classifications WHERE campaign_id = $1 ORDER BY created_at DESC LIMIT 1`,
    [campaignId]
  );
  // NUMERIC confidence comes back from node-pg as a JS string when read as a
  // bare column; the `::float8` cast above makes real Postgres return a real
  // number, but we still defensively coerce here so the
  // `TriageNoteEvidence.confidence: number | null` contract holds even if a
  // caller/mock hands back a string (e.g. an untyped test double, or a future
  // driver change that stops honoring the cast).
  const classification = classificationRes.rows[0] ?? null;

  const remediationRes = await postgresClient.query<RemediationRow>(
    `SELECT action_type, status, approved_by, approved_at::text AS approved_at
       FROM remediation_actions WHERE campaign_id = $1 ORDER BY created_at ASC`,
    [campaignId]
  );

  // Real indicator-URL join (Phase 16 evidence) — reports.evidence has NO url
  // field, so urls must come from here, not from the reports.evidence JSONB.
  const urlIndicatorsRes = await postgresClient.query<IndicatorUrlRow>(
    `SELECT i.value FROM indicators i
       JOIN messages m ON m.id = i.message_id
       JOIN reports r ON r.id = m.report_id
      WHERE r.campaign_id = $1 AND i.indicator_type = 'url'`,
    [campaignId]
  );
  const urls = urlIndicatorsRes.rows.map((row) => row.value);

  const primaryReport = reports[0] ?? null;
  let blastRadius: BlastRadiusResult;
  if (primaryReport) {
    const createdAt = new Date(primaryReport.created_at);
    // Best-available sender/recipient given only what the bounded reports
    // query above returns (title only) — getBlastRadius never throws on
    // sparse input, it degrades to `status: 'unavailable'`/empty counts.
    blastRadius = await getBlastRadius({
      sender: '',
      recipient: '',
      subject: primaryReport.title ?? '',
      dateWindow: {
        start: new Date(createdAt.getTime() - 24 * 60 * 60 * 1000),
        end: new Date(createdAt.getTime() + 24 * 60 * 60 * 1000),
      },
    });
  } else {
    blastRadius = { status: 'unavailable', reason: 'not_configured' };
  }

  const confidence = classification?.confidence == null ? null : Number(classification.confidence);

  const evidence: TriageNoteEvidence = {
    campaignId,
    reportCount: reports.length,
    companyName: primaryReport?.company_name ?? null,
    subject: primaryReport?.title ?? null,
    verdict: (classification?.verdict as TriageNoteEvidence['verdict']) ?? null,
    confidence,
    summary: classification?.summary ?? null,
    reasons: parseJsonArray(classification?.reasons),
    recommendedActions: parseJsonArray(classification?.recommended_actions),
    requiresApproval: classification?.requires_approval ?? false,
    blastRadius,
    remediationActions: remediationRes.rows.map((row) => ({
      actionType: row.action_type,
      status: row.status,
      approvedBy: row.approved_by,
      approvedAt: row.approved_at,
    })),
    urls,
  };

  const noteText = formatTriageNote(evidence);

  const client = getAutotaskClient();
  const tickets: TriageNotePostResult[] = [];
  for (const report of reports) {
    // Per-ticket try/catch is INSIDE the loop (not around it) so one
    // ticket's write failure never aborts the remaining writes (D-05).
    try {
      await client.createEntity('TicketNotes', {
        ticketID: Number(report.ticket_id),
        title: 'Phishing Triage Summary',
        description: noteText,
        noteType: 1, // Internal
        publish: 1,
      });
      tickets.push({ ticketId: report.ticket_id, posted: true });
    } catch (err) {
      console.error('[PHISHING-TRIAGE-NOTE] Failed to post note to ticket', report.ticket_id, err);
      tickets.push({
        ticketId: report.ticket_id,
        posted: false,
        error: err instanceof Error ? err.message : 'Unknown error',
      });
    }
  }

  return { noteText, tickets };
}

/**
 * Phase 23 D-02/D-03: posts a short, genuinely appreciative thank-you note
 * to every ticket linked to a USER_AWARENESS campaign — the `acknowledge_user`
 * delivery action. Unlike `generateAndPostTriageNote` (an internal evidence
 * dump), this note is customer-visible: `noteType: 18` ("Client Portal
 * Note", verified live against this tenant's TicketNotes field metadata)
 * with `publish: 1` left unchanged (an internal-staff-tier field, orthogonal
 * to client visibility — see triage-note-format.ts / 23-PATTERNS.md watch-out
 * flag #1). The body is a FIXED template with zero evidence/URL/classification
 * interpolation (T-23-01) — nothing from the parsed email or classification
 * reasons is ever placed in this note.
 *
 * Mirrors `generateAndPostTriageNote`'s per-ticket try/catch-in-loop error
 * isolation (D-05) and `{ noteText, tickets }` return shape.
 */
export async function generateAndPostAcknowledgment(campaignId: string): Promise<TriageNoteResult> {
  const reportsRes = await postgresClient.query<Pick<ReportRow, 'id' | 'ticket_id'>>(
    `SELECT id::text, ticket_id::text AS ticket_id
       FROM reports WHERE campaign_id = $1 ORDER BY created_at ASC`,
    [campaignId]
  );
  const reports = reportsRes.rows;

  const noteText = [
    'Thanks for reporting this one!',
    '',
    "Heads up — this was a simulated phishing test from our security awareness program, not a real threat. But your instinct to flag it was spot on, and that's exactly what keeps us protected when the real thing shows up.",
    '',
    'Nicely done.',
  ].join('\n');

  const client = getAutotaskClient();
  const tickets: TriageNotePostResult[] = [];
  for (const report of reports) {
    // Per-ticket try/catch is INSIDE the loop (not around it) so one
    // ticket's write failure never aborts the remaining writes (D-05).
    try {
      await client.createEntity('TicketNotes', {
        ticketID: Number(report.ticket_id),
        title: 'Thank You — Suspicious Email Reported',
        description: noteText,
        noteType: 18, // Client Portal Note — customer-visible (D-03)
        publish: 1,
      });
      tickets.push({ ticketId: report.ticket_id, posted: true });
    } catch (err) {
      console.error('[PHISHING-ACKNOWLEDGMENT] Failed to post note to ticket', report.ticket_id, err);
      tickets.push({
        ticketId: report.ticket_id,
        posted: false,
        error: err instanceof Error ? err.message : 'Unknown error',
      });
    }
  }

  return { noteText, tickets };
}

/**
 * Quick task 260717-v6c: posts a short, fixed-template customer-visible note
 * to every ticket linked to a campaign that a reviewer has marked as an
 * accidental report — an employee flagged a legitimate email by mistake.
 * Mirrors `generateAndPostAcknowledgment`'s structure exactly: same reports
 * lookup query, same per-ticket try/catch INSIDE the loop (D-05 isolation),
 * same noteType 18 ("Client Portal Note") / publish 1, same
 * `{ noteText, tickets }` return shape. The body is a FIXED template with
 * zero evidence/URL/classification interpolation (T-23-01 invariant) —
 * nothing from the parsed email or classification reasons is ever placed in
 * this note.
 */
export async function generateAndPostAccidentalReportNote(campaignId: string): Promise<TriageNoteResult> {
  const reportsRes = await postgresClient.query<Pick<ReportRow, 'id' | 'ticket_id'>>(
    `SELECT id::text, ticket_id::text AS ticket_id
       FROM reports WHERE campaign_id = $1 ORDER BY created_at ASC`,
    [campaignId]
  );
  const reports = reportsRes.rows;

  const noteText = [
    'Thanks for flagging this — after review, this turned out to be a legitimate email that was reported by mistake, not a phishing attempt.',
    '',
    "No action is needed on your part, and this report has been closed out. If anything ever looks off in the future, please keep reporting it — that's exactly the right move.",
  ].join('\n\n');

  const client = getAutotaskClient();
  const tickets: TriageNotePostResult[] = [];
  for (const report of reports) {
    // Per-ticket try/catch is INSIDE the loop (not around it) so one
    // ticket's write failure never aborts the remaining writes (D-05).
    try {
      await client.createEntity('TicketNotes', {
        ticketID: Number(report.ticket_id),
        title: 'Thank You — Report Reviewed',
        description: noteText,
        noteType: 18, // Client Portal Note — customer-visible
        publish: 1,
      });
      tickets.push({ ticketId: report.ticket_id, posted: true });
    } catch (err) {
      console.error('[PHISHING-ACCIDENTAL-REPORT] Failed to post note to ticket', report.ticket_id, err);
      tickets.push({
        ticketId: report.ticket_id,
        posted: false,
        error: err instanceof Error ? err.message : 'Unknown error',
      });
    }
  }

  return { noteText, tickets };
}
