/**
 * Phishing Detector
 *
 * Shared detection core called by both the webhook path (Plan 03) and the
 * cron sweep (Plan 03). Matches a ticket's title+description against the 8
 * locked DETECT-01 patterns, computes a content hash for D-04 idempotency,
 * gathers EVID-01 evidence, and upserts a single `reports` row per candidate
 * ticket — reprocessing only when the content hash changed.
 *
 * One deterministic, testable detector with no duplicated matching logic
 * between callers (CONTEXT.md discretion: "both call the same underlying
 * logic").
 */

import { createHash } from 'crypto';
import { postgresClient } from './postgres-client';
import { getAutotaskClient } from './autotask-factory';

// =============================================================================
// Pure detection logic — pattern matcher + content hash
// =============================================================================

/**
 * The 8 locked DETECT-01 patterns (case-insensitive substring match).
 */
export const KNOWN_PHISHING_PATTERNS: readonly string[] = [
  'Phishing Report',
  'Spam Alert',
  'Phishing Alert - Email Security Report',
  'KnowBe4 Phish Alert Report',
  'Source: KnowBe4 Phish Alert Button',
  'userSubmissionsReportMessage',
  'reported message destinations',
  'Microsoft directly',
];

/**
 * Case-insensitive substring match against the locked pattern list — mirrors
 * robotic-classifier.evaluateContains (.toLowerCase() + .includes() only,
 * NO regex, NO eval).
 */
export function matchesPhishingPatterns(
  title: string | null,
  description: string | null
): { flagged: boolean; matched: string[] } {
  const haystack = `${title ?? ''} ${description ?? ''}`.toLowerCase();
  const matched = KNOWN_PHISHING_PATTERNS.filter((pattern) =>
    haystack.includes(pattern.toLowerCase())
  );
  return { flagged: matched.length > 0, matched };
}

/**
 * sha256 over title+description only (D-04) — does NOT include
 * last_activity_date, status, or any bump-prone field, so status/assignee
 * churn never forces reprocessing.
 */
export function computePhishingContentHash(
  title: string | null,
  description: string | null
): string {
  return createHash('sha256')
    .update(JSON.stringify({ title: title ?? '', description: description ?? '' }))
    .digest('hex');
}

// =============================================================================
// Evidence capture + orchestration
// =============================================================================

export interface DetectableTicket {
  id: number;
  ticket_number: string | null;
  title: string | null;
  description: string | null;
  company_id: number | null;
  contact_id?: number | null;
  created_by_contact_id?: number | null;
}

interface EvidenceNote {
  id: number;
  title: string | null;
  description: string | null;
  note_type: number | null;
  creator_resource_id: number | null;
  created_at: string;
}

interface EvidenceTimeEntry {
  id: number;
  resource_id: number | null;
  entry_date: string | null;
  hours_worked: number | null;
  start_date_time: string | null;
  end_date_time: string | null;
}

interface EvidenceAttachment {
  fullPath: string;
  title: string;
  contentType?: string;
}

export interface EvidencePayload {
  company_name: string | null;
  notes: EvidenceNote[];
  time_entries: EvidenceTimeEntry[];
  attachments: EvidenceAttachment[];
}

/**
 * Gather EVID-01 evidence for a candidate ticket: company name, ticket notes,
 * time entries, and attachment metadata (never base64 `data` — content fetch
 * is Phase 16).
 */
export async function gatherTicketEvidence(
  ticket: DetectableTicket
): Promise<EvidencePayload> {
  const companyResult = await postgresClient.query<{ company_name: string | null }>(
    `SELECT company_name FROM companies WHERE id = $1`,
    [ticket.company_id]
  );
  const company_name = companyResult.rows[0]?.company_name ?? null;

  const notesResult = await postgresClient.query<EvidenceNote>(
    `SELECT id, title, description, note_type, creator_resource_id, created_at
       FROM ticket_notes
      WHERE ticket_id = $1
        AND is_deleted = false
      ORDER BY created_at`,
    [ticket.id]
  );

  const timeEntriesResult = await postgresClient.query<EvidenceTimeEntry>(
    `SELECT id, resource_id, entry_date, hours_worked, start_date_time, end_date_time
       FROM time_entries
      WHERE ticket_id = $1
        AND is_deleted = false
      ORDER BY entry_date`,
    [ticket.id]
  );

  let attachments: EvidenceAttachment[] = [];
  try {
    const rawAttachments = await getAutotaskClient().getAttachments('Tickets', ticket.id);
    attachments = rawAttachments.map((attachment) => ({
      fullPath: attachment.fullPath,
      title: attachment.title,
      contentType: attachment.contentType,
    }));
  } catch (error) {
    console.error('[PHISHING-DETECT] Failed to fetch attachments for ticket', ticket.id, error);
    attachments = [];
  }

  return {
    company_name,
    notes: notesResult.rows,
    time_entries: timeEntriesResult.rows,
    attachments,
  };
}

export interface DetectPhishingResult {
  flagged: boolean;
  reportId?: string;
  skippedUnchanged?: boolean;
}

/**
 * Shared entry point called by both the webhook path and the cron sweep.
 * Matches, hashes, checks the D-04 idempotency guard, gathers EVID-01
 * evidence, and upserts one `reports` row per candidate ticket.
 */
export async function detectPhishingTicket(
  ticket: DetectableTicket
): Promise<DetectPhishingResult> {
  const { flagged, matched } = matchesPhishingPatterns(ticket.title, ticket.description);
  if (!flagged) {
    return { flagged: false };
  }

  const contentHash = computePhishingContentHash(ticket.title, ticket.description);

  try {
    const existing = await postgresClient.query<{ id: string; content_hash: string }>(
      `SELECT id::text AS id, content_hash FROM reports WHERE ticket_id = $1`,
      [ticket.id]
    );

    if (existing.rowCount && existing.rowCount > 0 && existing.rows[0].content_hash === contentHash) {
      // Content unchanged (D-04): skip full reprocessing (title/description/
      // matched_patterns/content_hash rewriting would be a no-op anyway).
      // But the evidence snapshot (notes, time entries, attachments) can
      // still drift after the first detection — an analyst can add a note,
      // log time, or attach a file without touching title/description. Refresh
      // the evidence snapshot on every call regardless of the content-hash
      // gate so later phases never read a stale snapshot.
      const refreshedEvidence = await gatherTicketEvidence(ticket);
      await postgresClient.query(
        `UPDATE reports SET evidence = $1::jsonb, updated_at = NOW() WHERE ticket_id = $2`,
        [JSON.stringify(refreshedEvidence), ticket.id]
      );
      return { flagged: true, reportId: existing.rows[0].id, skippedUnchanged: true };
    }

    const evidence = await gatherTicketEvidence(ticket);

    const upsertResult = await postgresClient.query<{ id: string }>(
      `INSERT INTO reports (
         ticket_id, ticket_number, company_id, company_name, requester_contact_id,
         created_by_contact_id, title, description, matched_patterns, content_hash, evidence
       )
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11::jsonb)
       ON CONFLICT (ticket_id) DO UPDATE SET
         ticket_number = EXCLUDED.ticket_number,
         company_id = EXCLUDED.company_id,
         company_name = EXCLUDED.company_name,
         requester_contact_id = EXCLUDED.requester_contact_id,
         created_by_contact_id = EXCLUDED.created_by_contact_id,
         title = EXCLUDED.title,
         description = EXCLUDED.description,
         matched_patterns = EXCLUDED.matched_patterns,
         content_hash = EXCLUDED.content_hash,
         evidence = EXCLUDED.evidence,
         updated_at = NOW()
       RETURNING id::text AS id`,
      [
        ticket.id,
        ticket.ticket_number,
        ticket.company_id,
        evidence.company_name,
        ticket.contact_id ?? null,
        ticket.created_by_contact_id ?? null,
        ticket.title,
        ticket.description,
        JSON.stringify(matched),
        contentHash,
        JSON.stringify(evidence),
      ]
    );

    return { flagged: true, reportId: upsertResult.rows[0].id };
  } catch (error) {
    console.error('[PHISHING-DETECT] Failed to detect/persist report for ticket', ticket.id, error);
    throw error;
  }
}
