/**
 * Analyzer persistence: read/write to analyzer_analyses, analyzer_jobs, and
 * analyzer_shares.
 *
 * All writes go through the postgresClient singleton — no transactions are
 * needed for the row-per-analysis writes since each is independent and the
 * unique (ticket_number, analysis_version) constraint prevents duplicates.
 */

import postgresClient from '@/lib/services/postgres-client';
import {
  type AggregateFingerprint,
  type AnalyzerJob,
  type DeepAnalysisResponse,
  type JobStatus,
  type PersistedAnalysis,
  type StageExecutionRecord,
  type TaggedEvent,
} from '@/lib/types/analyzer';
import type { ITGlueDocReference } from '@/lib/types/analyzer';

export interface InsertAnalysisInput {
  ticket_number: string;
  autotask_ticket_id: number;
  content_hash: string;
  triggered_by_user_id: string | null;
  status: 'complete' | 'failed';
  /** When the analysis run finished (now() if undefined). */
  completed_at?: Date;

  /** anthropic | openrouter — defaults to 'anthropic' for back-compat. */
  provider?: 'anthropic' | 'openrouter';

  haiku_used: boolean;
  sonnet_used: boolean;
  opus_used: boolean;
  total_input_tokens: number;
  total_output_tokens: number;
  estimated_cost_usd: number;

  /** Final analysis content (after any Opus updates). null on failure. */
  analysis: DeepAnalysisResponse | null;
  filtered_noise_count: number;
  /**
   * LEGACY (phase 1). Per-stage trace dump. Retained for back-compat until
   * analyzer_stage_executions has full coverage and we drop the column.
   */
  model_traces: Record<string, unknown>;
  /** Phase 2: Stage 0 preprocessed event list at analysis time. */
  source_snapshot?: TaggedEvent[] | null;
  error_message?: string | null;
}

/**
 * Returns the next monotonic analysis_version for this ticket **and provider**.
 * Uses MAX(...)+1 — there is a small race if two workers call this
 * simultaneously, but the UNIQUE (ticket_number, provider, analysis_version)
 * constraint catches it: the loser sees a 23505 unique_violation and the
 * worker should retry with a fresh version number.
 */
export async function getNextAnalysisVersion(
  ticketNumber: string,
  provider: 'anthropic' | 'openrouter' = 'anthropic'
): Promise<number> {
  const res = await postgresClient.query<{ next_version: string }>(
    `SELECT COALESCE(MAX(analysis_version), 0) + 1 AS next_version
       FROM analyzer_analyses
       WHERE ticket_number = $1
         AND provider = $2`,
    [ticketNumber, provider]
  );
  return Number(res.rows[0].next_version);
}

/**
 * Idempotency check: returns the most recent COMPLETE analysis row whose
 * content_hash matches **for the given provider**, if any. Used to
 * short-circuit re-runs when the source data hasn't changed and `force=false`.
 *
 * Provider-scoped so a Claude run doesn't short-circuit a request for a
 * DeepSeek run (and vice versa) — the user wants a parallel analysis.
 */
export async function findExistingAnalysisByContentHash(
  ticketNumber: string,
  contentHash: string,
  provider: 'anthropic' | 'openrouter' = 'anthropic'
): Promise<{ id: string; analysis_version: number } | null> {
  const res = await postgresClient.query<{ id: string; analysis_version: string }>(
    `SELECT id::text AS id, analysis_version::text AS analysis_version
       FROM analyzer_analyses
       WHERE ticket_number = $1
         AND content_hash_at_analysis = $2
         AND provider = $3
         AND status = 'complete'
       ORDER BY analysis_version DESC
       LIMIT 1`,
    [ticketNumber, contentHash, provider]
  );
  if (res.rowCount === 0) return null;
  const row = res.rows[0];
  return { id: row.id, analysis_version: Number(row.analysis_version) };
}

/**
 * Insert a completed (or failed) analysis row. Returns the new row's id.
 *
 * Note: the unique (ticket_number, analysis_version) constraint catches racing
 * writers. Caller should re-fetch the next version and retry if it sees a
 * unique-violation error from postgres.
 */
export async function insertAnalysis(input: InsertAnalysisInput): Promise<{
  id: string;
  analysis_version: number;
}> {
  const provider = input.provider ?? 'anthropic';
  const version = await getNextAnalysisVersion(input.ticket_number, provider);
  const completedAt = input.completed_at ?? new Date();
  const a = input.analysis;

  const res = await postgresClient.query<{ id: string }>(
    `
    INSERT INTO analyzer_analyses (
      ticket_number, autotask_ticket_id, analysis_version,
      content_hash_at_analysis, triggered_by_user_id,
      status, completed_at,
      haiku_used, sonnet_used, opus_used,
      total_input_tokens, total_output_tokens, estimated_cost_usd,
      summary, timeline, what_was_done, what_should_have_been_done,
      gaps, next_step, next_step_rationale, post_resolution_analysis,
      confidence_score, needs_human_review, human_review_reasons,
      itglue_docs_referenced, model_traces, filtered_noise_count, error_message,
      source_snapshot, provider
    )
    VALUES (
      $1, $2, $3,
      $4, $5,
      $6, $7,
      $8, $9, $10,
      $11, $12, $13,
      $14, $15::jsonb, $16::jsonb, $17::jsonb,
      $18::jsonb, $19, $20, $21,
      $22, $23, $24::jsonb,
      $25::jsonb, $26::jsonb, $27, $28,
      $29::jsonb, $30
    )
    RETURNING id::text AS id
    `,
    [
      input.ticket_number,
      input.autotask_ticket_id,
      version,
      input.content_hash,
      input.triggered_by_user_id,
      input.status,
      completedAt,
      input.haiku_used,
      input.sonnet_used,
      input.opus_used,
      input.total_input_tokens,
      input.total_output_tokens,
      input.estimated_cost_usd,
      a?.summary ?? null,
      a?.timeline ? JSON.stringify(a.timeline) : null,
      a?.what_was_done ? JSON.stringify(a.what_was_done) : null,
      a?.what_should_have_been_done
        ? JSON.stringify(a.what_should_have_been_done)
        : null,
      a?.gaps ? JSON.stringify(a.gaps) : null,
      a?.next_step ?? null,
      a?.next_step_rationale ?? null,
      a?.post_resolution_analysis ?? null,
      a?.confidence_score ?? null,
      a?.needs_human_review ?? false,
      a?.human_review_reasons ? JSON.stringify(a.human_review_reasons) : null,
      JSON.stringify(a?.itglue_docs_referenced ?? []),
      JSON.stringify(input.model_traces),
      input.filtered_noise_count,
      input.error_message ?? null,
      input.source_snapshot ? JSON.stringify(input.source_snapshot) : null,
      provider,
    ]
  );

  return { id: res.rows[0].id, analysis_version: version };
}

/**
 * Bulk-insert one analyzer_stage_executions row per record. No-op when records
 * is empty. Single multi-VALUES INSERT — fast enough for the few rows produced
 * per pipeline run that we don't need COPY.
 */
export async function bulkInsertStageExecutions(
  analysisId: string,
  records: StageExecutionRecord[]
): Promise<void> {
  if (records.length === 0) return;
  const values: unknown[] = [analysisId];
  const tuples: string[] = [];
  for (const r of records) {
    const base = values.length;
    values.push(
      r.stage,
      r.stage_order,
      r.model_id,
      JSON.stringify(r.input_payload ?? {}),
      JSON.stringify(r.output_payload ?? {}),
      r.input_tokens,
      r.output_tokens,
      r.latency_ms,
      r.started_at,
      r.completed_at,
      r.error_message
    );
    tuples.push(
      `($1, $${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, ` +
        `$${base + 5}::jsonb, $${base + 6}, $${base + 7}, $${base + 8}, ` +
        `$${base + 9}, $${base + 10}, $${base + 11})`
    );
  }
  await postgresClient.query(
    `INSERT INTO analyzer_stage_executions (
       analysis_id, stage, stage_order, model_id,
       input_payload, output_payload,
       input_tokens, output_tokens, latency_ms,
       started_at, completed_at, error_message
     ) VALUES ${tuples.join(', ')}`,
    values
  );
}

/**
 * Phase 2: write the Stage 6 fingerprint to an existing analysis row.
 */
export async function updateAnalysisFingerprint(
  analysisId: string,
  fingerprint: AggregateFingerprint
): Promise<void> {
  await postgresClient.query(
    `UPDATE analyzer_analyses
       SET aggregate_fingerprint = $2::jsonb,
           fingerprint_generated_at = NOW()
     WHERE id = $1`,
    [analysisId, JSON.stringify(fingerprint)]
  );
}

/**
 * Phase 2: persist a 'failed' analyzer_analyses row when the pipeline throws.
 * Carries content_hash + source_snapshot so partial-run forensics work, plus
 * any stage records the pipeline managed to record before throwing.
 */
export async function insertFailedAnalysis(input: {
  ticket_number: string;
  autotask_ticket_id: number;
  content_hash: string;
  triggered_by_user_id: string | null;
  source_snapshot: TaggedEvent[];
  filtered_noise_count: number;
  error_message: string;
  partial_input_tokens: number;
  partial_output_tokens: number;
  partial_cost_usd: number;
  haiku_used: boolean;
  sonnet_used: boolean;
  opus_used: boolean;
  provider?: 'anthropic' | 'openrouter';
}): Promise<{ id: string; analysis_version: number }> {
  return await insertAnalysis({
    ticket_number: input.ticket_number,
    autotask_ticket_id: input.autotask_ticket_id,
    content_hash: input.content_hash,
    triggered_by_user_id: input.triggered_by_user_id,
    status: 'failed',
    haiku_used: input.haiku_used,
    sonnet_used: input.sonnet_used,
    opus_used: input.opus_used,
    total_input_tokens: input.partial_input_tokens,
    total_output_tokens: input.partial_output_tokens,
    estimated_cost_usd: input.partial_cost_usd,
    analysis: null,
    filtered_noise_count: input.filtered_noise_count,
    model_traces: {},
    source_snapshot: input.source_snapshot,
    error_message: input.error_message,
    provider: input.provider,
  });
}

// =============================================================================
// Job table operations
// =============================================================================

/**
 * Try to claim the oldest queued job. Atomic via UPDATE ... WHERE ... RETURNING.
 * Returns null if no queued jobs are available.
 */
export async function claimQueuedJob(): Promise<{
  id: string;
  ticket_number: string;
  queued_by_user_id: string | null;
  provider: 'anthropic' | 'openrouter';
} | null> {
  const res = await postgresClient.query<{
    id: string;
    ticket_number: string;
    queued_by_user_id: string | null;
    provider: 'anthropic' | 'openrouter';
  }>(
    `
    UPDATE analyzer_jobs
    SET status = 'fetching', started_at = NOW()
    WHERE id = (
      SELECT id FROM analyzer_jobs
      WHERE status = 'queued'
      ORDER BY queued_at
      FOR UPDATE SKIP LOCKED
      LIMIT 1
    )
    RETURNING id::text AS id, ticket_number, queued_by_user_id, provider
    `
  );
  if (res.rowCount === 0) return null;
  return res.rows[0];
}

export async function updateJobStatus(
  jobId: string,
  status: JobStatus
): Promise<void> {
  await postgresClient.query(
    `UPDATE analyzer_jobs SET status = $1 WHERE id = $2`,
    [status, jobId]
  );
}

export async function completeJob(
  jobId: string,
  resultAnalysisId: string
): Promise<void> {
  await postgresClient.query(
    `UPDATE analyzer_jobs
       SET status = 'complete',
           result_analysis_id = $1,
           finished_at = NOW()
       WHERE id = $2`,
    [resultAnalysisId, jobId]
  );
}

/**
 * Reclaim orphaned in-flight jobs whose started_at is older than the threshold.
 * Called once on worker boot — any job in an active state (fetching/triaging/
 * itglue/analyzing/deep_review) that's been "running" longer than the expected
 * pipeline ceiling is almost certainly orphaned by a container restart and
 * needs to be re-queued. Returns the number of rows reset.
 */
export async function resetStaleJobsToQueued(
  thresholdMinutes = 10
): Promise<number> {
  const res = await postgresClient.query(
    `UPDATE analyzer_jobs
        SET status = 'queued',
            started_at = NULL
      WHERE status IN ('fetching','triaging','itglue','analyzing','deep_review')
        AND started_at IS NOT NULL
        AND started_at < NOW() - ($1::int || ' minutes')::interval`,
    [thresholdMinutes]
  );
  return res.rowCount ?? 0;
}

export async function failJob(jobId: string, errorMessage: string): Promise<void> {
  await postgresClient.query(
    `UPDATE analyzer_jobs
       SET status = 'failed',
           error_message = $1,
           finished_at = NOW()
       WHERE id = $2`,
    [errorMessage, jobId]
  );
}

export interface QueueJobInput {
  ticket_number: string;
  queued_by_user_id: string | null;
  provider?: 'anthropic' | 'openrouter';
}

export async function queueJob(input: QueueJobInput): Promise<{ id: string }> {
  const res = await postgresClient.query<{ id: string }>(
    `INSERT INTO analyzer_jobs (ticket_number, queued_by_user_id, provider)
       VALUES ($1, $2, $3)
       RETURNING id::text AS id`,
    [input.ticket_number, input.queued_by_user_id, input.provider ?? 'anthropic']
  );
  return { id: res.rows[0].id };
}

export async function getJob(jobId: string): Promise<AnalyzerJob | null> {
  const res = await postgresClient.query<{
    id: string;
    ticket_number: string;
    queued_by_user_id: string | null;
    status: JobStatus;
    result_analysis_id: string | null;
    queued_at: Date;
    started_at: Date | null;
    finished_at: Date | null;
    error_message: string | null;
  }>(
    `SELECT id::text AS id, ticket_number, queued_by_user_id, status,
            result_analysis_id::text AS result_analysis_id,
            queued_at, started_at, finished_at, error_message
       FROM analyzer_jobs WHERE id = $1`,
    [jobId]
  );
  if (res.rowCount === 0) return null;
  const r = res.rows[0];
  return {
    id: r.id,
    ticketNumber: r.ticket_number,
    queuedByUserId: r.queued_by_user_id,
    status: r.status,
    resultAnalysisId: r.result_analysis_id,
    queuedAt: r.queued_at.toISOString(),
    startedAt: r.started_at ? r.started_at.toISOString() : null,
    finishedAt: r.finished_at ? r.finished_at.toISOString() : null,
    errorMessage: r.error_message,
  };
}

// =============================================================================
// Read paths used by the API routes
// =============================================================================

interface AnalysisRow {
  id: string;
  ticket_number: string;
  autotask_ticket_id: string;
  analysis_version: string;
  content_hash_at_analysis: string;
  triggered_by_user_id: string | null;
  triggered_at: Date;
  status: PersistedAnalysis['status'];
  completed_at: Date | null;
  haiku_used: boolean;
  sonnet_used: boolean;
  opus_used: boolean;
  total_input_tokens: number;
  total_output_tokens: number;
  estimated_cost_usd: string;
  summary: string | null;
  timeline: unknown;
  what_was_done: unknown;
  what_should_have_been_done: unknown;
  gaps: unknown;
  next_step: string | null;
  next_step_rationale: string | null;
  post_resolution_analysis: string | null;
  confidence_score: string | null;
  needs_human_review: boolean;
  human_review_reasons: unknown;
  itglue_docs_referenced: unknown;
  filtered_noise_count: number;
  error_message: string | null;
  provider: 'anthropic' | 'openrouter';
}

function rowToPersistedAnalysis(r: AnalysisRow): PersistedAnalysis {
  return {
    id: r.id,
    ticketNumber: r.ticket_number,
    autotaskTicketId: Number(r.autotask_ticket_id),
    analysisVersion: Number(r.analysis_version),
    contentHashAtAnalysis: r.content_hash_at_analysis,
    triggeredByUserId: r.triggered_by_user_id,
    triggeredAt: r.triggered_at.toISOString(),
    status: r.status,
    completedAt: r.completed_at ? r.completed_at.toISOString() : null,
    haikuUsed: r.haiku_used,
    sonnetUsed: r.sonnet_used,
    opusUsed: r.opus_used,
    totalInputTokens: r.total_input_tokens,
    totalOutputTokens: r.total_output_tokens,
    estimatedCostUsd: Number(r.estimated_cost_usd),
    summary: r.summary,
    // JSONB columns deserialize directly to JS objects in node-postgres; cast
    // to the schema type. We trust Zod-validated writes from the pipeline.
    timeline: r.timeline as PersistedAnalysis['timeline'],
    whatWasDone: r.what_was_done as PersistedAnalysis['whatWasDone'],
    whatShouldHaveBeenDone:
      r.what_should_have_been_done as PersistedAnalysis['whatShouldHaveBeenDone'],
    gaps: r.gaps as PersistedAnalysis['gaps'],
    nextStep: r.next_step,
    nextStepRationale: r.next_step_rationale,
    postResolutionAnalysis: r.post_resolution_analysis,
    confidenceScore:
      r.confidence_score === null ? null : Number(r.confidence_score),
    needsHumanReview: r.needs_human_review,
    humanReviewReasons: r.human_review_reasons as
      | PersistedAnalysis['humanReviewReasons'],
    itglueDocsReferenced:
      (r.itglue_docs_referenced as PersistedAnalysis['itglueDocsReferenced']) ?? [],
    filteredNoiseCount: r.filtered_noise_count,
    errorMessage: r.error_message,
    provider: r.provider,
  };
}

const ANALYSIS_SELECT = `
  id::text                                AS id,
  ticket_number,
  autotask_ticket_id::text                AS autotask_ticket_id,
  analysis_version::text                  AS analysis_version,
  content_hash_at_analysis,
  triggered_by_user_id,
  triggered_at,
  status,
  completed_at,
  haiku_used, sonnet_used, opus_used,
  total_input_tokens, total_output_tokens, estimated_cost_usd::text AS estimated_cost_usd,
  summary, timeline, what_was_done, what_should_have_been_done,
  gaps, next_step, next_step_rationale, post_resolution_analysis,
  confidence_score::text                  AS confidence_score,
  needs_human_review,
  human_review_reasons,
  itglue_docs_referenced,
  filtered_noise_count,
  error_message,
  provider
`;

export async function getAnalysisById(
  id: string
): Promise<PersistedAnalysis | null> {
  const res = await postgresClient.query<AnalysisRow>(
    `SELECT ${ANALYSIS_SELECT} FROM analyzer_analyses WHERE id = $1`,
    [id]
  );
  if (res.rowCount === 0) return null;
  return rowToPersistedAnalysis(res.rows[0]);
}

export async function listAnalysesByTicketNumber(
  ticketNumber: string
): Promise<PersistedAnalysis[]> {
  // Order chronologically (most recent first) so the latest run shows up at
  // the top of the history regardless of provider. Two providers maintain
  // their own monotonic version numbers, so a strict version sort would
  // interleave them oddly.
  const res = await postgresClient.query<AnalysisRow>(
    `SELECT ${ANALYSIS_SELECT}
       FROM analyzer_analyses
       WHERE ticket_number = $1
       ORDER BY triggered_at DESC, analysis_version DESC`,
    [ticketNumber]
  );
  return res.rows.map(rowToPersistedAnalysis);
}

export async function listNeedsReview(opts: {
  limit?: number;
  offset?: number;
} = {}): Promise<PersistedAnalysis[]> {
  const limit = Math.min(opts.limit ?? 50, 200);
  const offset = opts.offset ?? 0;
  const res = await postgresClient.query<AnalysisRow>(
    `SELECT ${ANALYSIS_SELECT}
       FROM analyzer_analyses
       WHERE needs_human_review = true
         AND status = 'complete'
       ORDER BY triggered_at DESC
       LIMIT $1 OFFSET $2`,
    [limit, offset]
  );
  return res.rows.map(rowToPersistedAnalysis);
}

// =============================================================================
// Share log
// =============================================================================

export interface CreateShareInput {
  analysis_id: string;
  shared_by_user_id: string;
  shared_with_email: string;
  note?: string | null;
}

export async function createShare(
  input: CreateShareInput
): Promise<{ id: string; shared_at: string }> {
  const res = await postgresClient.query<{ id: string; shared_at: Date }>(
    `INSERT INTO analyzer_shares
       (analysis_id, shared_by_user_id, shared_with_email, note)
       VALUES ($1, $2, $3, $4)
       RETURNING id::text AS id, shared_at`,
    [
      input.analysis_id,
      input.shared_by_user_id,
      input.shared_with_email,
      input.note ?? null,
    ]
  );
  return {
    id: res.rows[0].id,
    shared_at: res.rows[0].shared_at.toISOString(),
  };
}

// Re-export types referenced elsewhere.
export type { TaggedEvent, ITGlueDocReference, PersistedAnalysis };
