/**
 * Stage 0 — pre-processor for the AI Ticket Analyzer pipeline.
 *
 * Takes a raw ticket bundle (header + ticket notes + time entries, joined
 * with creator names/emails by the data-access layer) and produces a
 * deterministic, tagged, chronological event timeline ready for the Haiku
 * triage stage.
 *
 * Responsibilities, per docs/wulf-pulse-ticket-analyzer-prompt.md:
 *   1. Filter workflow-rule noise and email-notification rows.
 *   2. Tag each retained event with actor, actor_type, source, visibility.
 *   3. Sort chronologically.
 *   4. Compute a stable content hash for idempotency.
 *
 * No LLM calls happen here. This module is fully deterministic and tested.
 */

import { createHash } from 'crypto';
import {
  type ActorType,
  type EventSource,
  type Visibility,
  type TaggedEvent,
  type PreprocessedTicket,
  type TicketHeaderForLLM,
} from '@/lib/types/analyzer';

// =============================================================================
// Input types — what the data-access layer feeds us.
// =============================================================================

export interface RawTicketHeader {
  id: number;
  ticket_number: string;
  title: string;
  description: string | null;
  status: number;
  status_label: string | null;
  priority: number;
  priority_label: string | null;
  queue_id: number | null;
  queue_label: string | null;
  company_id: number;
  company_name: string | null;
  contact_id: number | null;
  contact_name: string | null;
  contact_email: string | null;
  assigned_resource_id: number | null;
  assignee_name: string | null;
  assignee_email: string | null;
  create_date: string;
  last_activity_date: string;
  resolved_date_time: string | null;
  problem_ticket_id: number | null;
}

export interface RawTicketNote {
  id: number;
  title: string | null;
  description: string;
  note_type: number | null;
  publish: number | null;
  creator_resource_id: number | null;
  creator_name: string | null;
  creator_email: string | null;
  creator_type: number | null;
  create_date_time: string | null;
}

export interface RawTimeEntry {
  id: number;
  resource_id: number;
  resource_name: string | null;
  resource_email: string | null;
  hours_worked: number;
  notes: string | null;
  internal_notes: string | null;
  entry_date: string | null;
  start_date_time: string | null;
  end_date_time: string | null;
  type: number | null;
}

export interface RawTicketBundle {
  ticket: RawTicketHeader;
  notes: RawTicketNote[];
  time_entries: RawTimeEntry[];
}

// =============================================================================
// Filtering — strip rows the analyzer should never see.
// =============================================================================

const AUTOTASK_ADMINISTRATOR_RESOURCE_ID = 4;

/**
 * Workflow-rule firings have title "Workflow Rule \"X\" fired." and creator
 * resource id 4 (the Autotask Administrator service account). The two checks
 * are belt-and-braces: title alone is sufficient in practice, but the resource
 * id catches edge cases where the title format changes.
 */
export function isWorkflowNoise(note: RawTicketNote): boolean {
  if (note.creator_resource_id === AUTOTASK_ADMINISTRATOR_RESOURCE_ID) return true;
  if (note.title?.startsWith('Workflow Rule')) return true;
  return false;
}

/**
 * "Service Desk Notification" rows are auto-generated email send confirmations
 * (the description is just a comma-separated recipient list).
 */
export function isEmailNotification(note: RawTicketNote): boolean {
  return note.title === 'Service Desk Notification';
}

// =============================================================================
// Actor-type classification — by domain, NOT by author.
// =============================================================================

const WULF_DOMAIN = 'wulfconsulting.com';
// Conservative vendor list — the LLM stages can refine. Add only domains we're
// confident about; misclassifying a customer domain as "vendor" is worse than
// the default "client_contact".
const VENDOR_DOMAINS = new Set<string>([
  'vertafore.com',
  'autotask.com',
  'datto.com',
  'microsoft.com',
  'auvik.com',
  'addigy.com',
  'sentinelone.com',
  'mimecast.com',
  'itglue.com',
  'duo.com',
  'duosecurity.com',
]);

export function classifyActorType(
  email: string | null | undefined,
  creatorResourceId: number | null = null
): ActorType {
  if (creatorResourceId === AUTOTASK_ADMINISTRATOR_RESOURCE_ID) return 'automation';
  if (!email) return 'system';

  const domain = email.split('@')[1]?.toLowerCase();
  if (!domain) return 'system';

  if (domain === WULF_DOMAIN) return 'wulf_tech';
  if (VENDOR_DOMAINS.has(domain)) return 'vendor';
  return 'client_contact';
}

// =============================================================================
// Tagging — turn raw rows into TaggedEvents.
// =============================================================================

/**
 * Map Autotask `publish` picklist to our visibility tag.
 *   1 = All Internal and External Users → customer_facing
 *   2 = Internal Users Only             → internal_only
 *   anything else                       → customer_facing (safe default)
 *
 * Notification rows (publish=4) are filtered upstream before this runs.
 */
function publishToVisibility(publish: number | null): Visibility {
  if (publish === 2) return 'internal_only';
  return 'customer_facing';
}

export function tagTicketCreate(header: RawTicketHeader): TaggedEvent | null {
  // The header description is the body of the ticket as opened. Some tickets
  // are opened with no description; emit nothing rather than a null event.
  if (!header.description) return null;

  return {
    timestamp: header.create_date,
    actor: header.contact_name ?? header.assignee_name ?? 'Unknown',
    actor_type: classifyActorType(header.contact_email),
    source: 'ticket_create' as EventSource,
    visibility: 'customer_facing',
    summary_notes: header.description,
  };
}

export function tagTicketNote(note: RawTicketNote): TaggedEvent | null {
  if (!note.create_date_time) return null;

  const visibility = publishToVisibility(note.publish);
  const event: TaggedEvent = {
    timestamp: note.create_date_time,
    actor: note.creator_name ?? 'Unknown',
    actor_type: classifyActorType(note.creator_email, note.creator_resource_id),
    source: 'ticket_note' as EventSource,
    visibility,
  };
  if (visibility === 'customer_facing') {
    event.summary_notes = note.description;
  } else {
    event.internal_notes = note.description;
  }
  return event;
}

export function tagTimeEntry(entry: RawTimeEntry): TaggedEvent | null {
  const hasSummary = !!entry.notes && entry.notes.trim().length > 0;
  const hasInternal = !!entry.internal_notes && entry.internal_notes.trim().length > 0;

  // No content at all → drop. A purely numeric time entry doesn't add narrative.
  if (!hasSummary && !hasInternal) return null;

  const timestamp = entry.end_date_time ?? entry.start_date_time ?? entry.entry_date;
  if (!timestamp) return null;

  const visibility: Visibility = hasSummary && hasInternal
    ? 'mixed'
    : hasSummary
      ? 'customer_facing'
      : 'internal_only';

  const event: TaggedEvent = {
    timestamp,
    actor: entry.resource_name ?? 'Unknown',
    actor_type: classifyActorType(entry.resource_email),
    source: 'time_entry' as EventSource,
    visibility,
    hours: entry.hours_worked,
  };
  if (hasSummary) event.summary_notes = entry.notes!;
  if (hasInternal) event.internal_notes = entry.internal_notes!;
  return event;
}

// =============================================================================
// Content hash — sha256 over canonical JSON of (events, status, priority, queue).
// =============================================================================

/**
 * Stable JSON: object keys sorted recursively, arrays preserved in order.
 * Used so the hash is deterministic regardless of property insertion order.
 */
function canonicalize(value: unknown): unknown {
  if (value === null || typeof value !== 'object') return value;
  if (Array.isArray(value)) return value.map(canonicalize);
  const out: Record<string, unknown> = {};
  for (const key of Object.keys(value as Record<string, unknown>).sort()) {
    out[key] = canonicalize((value as Record<string, unknown>)[key]);
  }
  return out;
}

export function computeContentHash(
  events: TaggedEvent[],
  ticketStatus: number,
  ticketPriority: number,
  queueId: number | null
): string {
  const canonical = JSON.stringify(
    canonicalize({
      events,
      status: ticketStatus,
      priority: ticketPriority,
      queue: queueId,
    })
  );
  return createHash('sha256').update(canonical).digest('hex');
}

// =============================================================================
// Top-level — preprocess one ticket bundle.
// =============================================================================

export function preprocessTicket(bundle: RawTicketBundle): PreprocessedTicket {
  const { ticket, notes, time_entries } = bundle;

  // 1. Filter noise.
  let filteredNoise = 0;
  const retainedNotes = notes.filter((n) => {
    if (isWorkflowNoise(n) || isEmailNotification(n)) {
      filteredNoise += 1;
      return false;
    }
    return true;
  });

  // 2. Tag.
  const events: TaggedEvent[] = [];
  const ticketCreate = tagTicketCreate(ticket);
  if (ticketCreate) events.push(ticketCreate);
  for (const n of retainedNotes) {
    const tagged = tagTicketNote(n);
    if (tagged) events.push(tagged);
  }
  for (const e of time_entries) {
    const tagged = tagTimeEntry(e);
    if (tagged) events.push(tagged);
  }

  // Add a resolution event if the ticket is resolved.
  if (ticket.resolved_date_time) {
    events.push({
      timestamp: ticket.resolved_date_time,
      actor: ticket.assignee_name ?? 'Unknown',
      actor_type: classifyActorType(ticket.assignee_email),
      source: 'resolution' as EventSource,
      visibility: 'customer_facing',
    });
  }

  // 3. Sort chronologically. Tie-break by source so deterministic regardless
  //    of input ordering.
  events.sort((a, b) => {
    const t = a.timestamp.localeCompare(b.timestamp);
    if (t !== 0) return t;
    return a.source.localeCompare(b.source);
  });

  // 4. Counts.
  let customer_facing = 0;
  let internal_only = 0;
  let mixed = 0;
  for (const e of events) {
    if (e.visibility === 'customer_facing') customer_facing += 1;
    else if (e.visibility === 'internal_only') internal_only += 1;
    else mixed += 1;
  }

  // 5. Header for the LLM payload.
  const header: TicketHeaderForLLM = {
    ticket_number: ticket.ticket_number,
    autotask_ticket_id: ticket.id,
    title: ticket.title,
    status_label: ticket.status_label ?? `status_${ticket.status}`,
    priority_label: ticket.priority_label,
    queue: ticket.queue_label,
    account_name: ticket.company_name,
    contact_name: ticket.contact_name,
    contact_email: ticket.contact_email,
    created_at: ticket.create_date,
    resolved_at: ticket.resolved_date_time,
  };

  // 6. Content hash.
  const content_hash = computeContentHash(
    events,
    ticket.status,
    ticket.priority,
    ticket.queue_id
  );

  return {
    header,
    events,
    counts: {
      total_events: events.length,
      customer_facing,
      internal_only,
      mixed,
      filtered_noise: filteredNoise,
    },
    content_hash,
  };
}
