import 'server-only'

import { prisma } from '../db'
import {
  findSimilar,
  proposeLearning,
  type LearningCandidate,
  type MemoryRecord,
  type SimilarDecision,
  type SituationContext,
} from '../engine/decision-memory'
import { round } from '../engine/rng'
import type { AssetIntelligence } from './intelligence-service'

/**
 * Decision memory and the governed learning queue (§27–§33).
 *
 * The single rule this file exists to enforce, from §30: **a simulated success and an
 * observed success are never merged.** They are written with different evidence kinds,
 * ranked with different confidence ceilings, and surfaced with different labels. Every
 * function here carries `evidenceKind` through rather than dropping it at a boundary,
 * because the moment it is dropped the platform starts claiming it has prevented real
 * failures — which is precisely the claim §122 forbids.
 *
 * Nothing here writes to a model, a threshold or a strategy weight. `proposeLearning`
 * produces candidates for a person to approve; the approval path updates the candidate's
 * status and nothing else (§32).
 */

const LOCALE = 'en-US'

/** The situation as the memory sees it — the fields similarity is computed over. */
export function contextFromIntelligence(intel: AssetIntelligence): SituationContext {
  const at = new Date(intel.ts)
  return {
    assetCode: intel.assetCode,
    regionCode: intel.focus.profile.regionCode,
    assetTypeKey: intel.focus.profile.assetTypeKey,
    loadPct: round(Math.abs(intel.focus.sample.loadPct), 1),
    tempC: round(intel.focus.sample.tempC, 1),
    ambientC: round(intel.focus.sample.ambientC, 1),
    healthScore: round(intel.focus.profile.healthScore, 1),
    riskBefore: round(intel.orchestration.unifiedRisk, 1),
    hourOfDay: at.getUTCHours(),
    monthOfYear: at.getUTCMonth() + 1,
    dnaPatternCode: intel.dnaMatches[0]?.pattern.code ?? '',
    dnaSimilarity: round(intel.dnaMatches[0]?.similarity ?? 0, 1),
    rootCauseKey: intel.rootCause.primary?.key ?? '',
    cascadeDepth: intel.cascade.maxDepth,
  }
}

/** Every remembered decision, in the shape the similarity engine expects. */
export async function loadMemory(options: { limit?: number } = {}): Promise<MemoryRecord[]> {
  const rows = await prisma.decisionMemory.findMany({
    orderBy: { createdAt: 'desc' },
    take: options.limit ?? 200,
  })

  return rows.map((row) => ({
    code: row.code,
    assetCode: row.assetCode,
    regionCode: row.regionCode,
    assetTypeKey: row.assetTypeKey,
    title: row.title,
    titleAr: row.titleAr,
    loadPct: row.loadPct,
    tempC: row.tempC,
    ambientC: row.ambientC,
    healthScore: row.healthScore,
    riskBefore: row.riskBefore,
    hourOfDay: row.hourOfDay,
    monthOfYear: row.monthOfYear,
    dnaPatternCode: row.dnaPatternCode,
    dnaSimilarity: row.dnaSimilarity,
    rootCauseKey: row.rootCauseKey,
    cascadeDepth: row.cascadeDepth,
    strategyKey: row.strategyKey,
    strategyLabel: row.strategyLabel,
    strategyLabelAr: row.strategyLabelAr,
    riskAfter: row.riskAfter,
    evidenceKind: row.evidenceKind === 'observed' ? 'observed' : 'simulated',
    outcomeStatus: row.outcomeStatus as MemoryRecord['outcomeStatus'],
    createdAt: row.createdAt.getTime(),
  }))
}

export interface SimilarDecisionView extends SimilarDecision {
  /** Rendered once here so every surface phrases the caveat identically. */
  evidenceLabel: string
  evidenceLabelAr: string
}

/**
 * "Have we seen this before?"
 *
 * Returns matches ranked by similarity with their per-field contributions attached, so a
 * 91 % match can be justified on screen rather than merely asserted.
 */
export async function findSimilarDecisions(
  context: SituationContext,
  options: { limit?: number; minSimilarity?: number } = {},
): Promise<SimilarDecisionView[]> {
  const history = await loadMemory()
  // A decision cannot be similar to itself: exclude anything recorded for this asset in
  // the same hour, which is what a repeat analysis of the same situation looks like.
  const filtered = history.filter(
    (record) =>
      !(record.assetCode === context.assetCode && record.hourOfDay === context.hourOfDay && record.monthOfYear === context.monthOfYear),
  )

  return findSimilar(context, filtered, options).map((match) => ({
    ...match,
    evidenceLabel:
      match.record.evidenceKind === 'observed'
        ? 'Observed outcome'
        : 'Simulated outcome — not a record of a prevented failure',
    evidenceLabelAr:
      match.record.evidenceKind === 'observed'
        ? 'نتيجة ملاحظة'
        : 'نتيجة محاكاة — ليست سجلًا لعطل تم منعه فعليًا',
  }))
}

export interface RememberInput {
  intel: AssetIntelligence
  strategyKey: string
  strategyLabel: string
  strategyLabelAr: string
  riskAfter: number
  decisionId?: string | null
  approverId?: string | null
  approvedAt?: number | null
  evidenceKind?: 'simulated' | 'observed'
}

/**
 * Write a decision into memory.
 *
 * Defaults to `simulated`, and the caller has to say otherwise explicitly. That default
 * is the safe one: in this build every outcome is produced by the simulation engine, and
 * a memory that quietly defaults to "observed" would turn the whole learning loop into a
 * fabrication.
 */
export async function rememberDecision(input: RememberInput): Promise<string> {
  const context = contextFromIntelligence(input.intel)
  const code = `DM-${Date.now().toString(36).toUpperCase()}`

  await prisma.decisionMemory.create({
    data: {
      code,
      decisionId: input.decisionId ?? null,
      assetCode: context.assetCode,
      regionCode: context.regionCode,
      assetTypeKey: context.assetTypeKey,
      title: `${input.strategyLabel} on ${context.assetCode}`,
      titleAr: `${input.strategyLabelAr} على ${context.assetCode}`,
      loadPct: context.loadPct,
      tempC: context.tempC,
      ambientC: context.ambientC,
      healthScore: context.healthScore,
      riskBefore: context.riskBefore,
      hourOfDay: context.hourOfDay,
      monthOfYear: context.monthOfYear,
      dnaPatternCode: context.dnaPatternCode,
      dnaSimilarity: context.dnaSimilarity,
      rootCauseKey: context.rootCauseKey,
      cascadeDepth: context.cascadeDepth,
      strategyKey: input.strategyKey,
      strategyLabel: input.strategyLabel,
      strategyLabelAr: input.strategyLabelAr,
      riskAfter: round(input.riskAfter, 1),
      approverId: input.approverId ?? null,
      approvedAt: input.approvedAt ? new Date(input.approvedAt) : null,
      evidenceKind: input.evidenceKind ?? 'simulated',
      outcomeStatus: 'pending',
    },
  })

  return code
}

/** Close the loop on a remembered decision. */
export async function recordOutcome(input: {
  memoryCode: string
  result: 'prevented' | 'occurred' | 'partial' | 'not_observed'
  observedRisk?: number | null
  evidenceKind?: 'simulated' | 'observed'
  notes?: string
  notesAr?: string
  userId?: string | null
}): Promise<boolean> {
  const memory = await prisma.decisionMemory.findUnique({ where: { code: input.memoryCode } })
  if (!memory) return false

  const evidenceKind = input.evidenceKind ?? 'simulated'

  await prisma.$transaction([
    prisma.decisionOutcome.create({
      data: {
        memoryId: memory.id,
        result: input.result,
        observedRisk: input.observedRisk ?? null,
        predictedRisk: memory.riskAfter,
        deltaRisk:
          input.observedRisk === null || input.observedRisk === undefined
            ? 0
            : round(input.observedRisk - memory.riskAfter, 2),
        observedAt: new Date(),
        evidenceKind,
        notes: input.notes ?? '',
        notesAr: input.notesAr ?? '',
        recordedById: input.userId ?? null,
      },
    }),
    prisma.decisionMemory.update({
      where: { id: memory.id },
      data: {
        outcomeStatus: input.result,
        // The memory's own evidence kind is only promoted to observed when the *outcome*
        // was observed. A simulated decision with an observed outcome is a real thing;
        // an observed decision with a simulated outcome is not, and must not be created
        // by a silent overwrite.
        evidenceKind: evidenceKind === 'observed' ? 'observed' : memory.evidenceKind,
      },
    }),
  ])

  return true
}

/** Add a human-written lesson to a remembered decision. */
export async function addLesson(input: {
  memoryCode: string
  text: string
  textAr?: string
  userId?: string | null
}): Promise<boolean> {
  const memory = await prisma.decisionMemory.findUnique({
    where: { code: input.memoryCode },
    select: { id: true },
  })
  if (!memory) return false

  await prisma.decisionLesson.create({
    data: {
      memoryId: memory.id,
      text: input.text,
      textAr: input.textAr ?? '',
      authorId: input.userId ?? null,
      isApproved: false,
    },
  })
  return true
}

// ─────────────────────────────────────────────────────────────────────────────
// The learning queue (§32, §33)
// ─────────────────────────────────────────────────────────────────────────────

export interface LearningCandidateView extends LearningCandidate {
  code: string
  status: string
  reviewedAt: number | null
  reviewNote: string
  createdAt: number
}

/**
 * Look over the memory and propose what the system appears to have learned.
 *
 * New candidates are written with status `new`. Anything whose title already exists is
 * left alone rather than duplicated — a queue that regenerates the same three proposals
 * on every run is a queue nobody reads.
 */
export async function refreshLearningCandidates(): Promise<number> {
  const history = await loadMemory()
  const proposals = proposeLearning(history)
  if (!proposals.length) return 0

  const existing = new Set(
    (await prisma.learningCandidate.findMany({ select: { title: true } })).map((row) => row.title),
  )

  const fresh = proposals.filter((candidate) => !existing.has(candidate.title))
  if (!fresh.length) return 0

  await prisma.learningCandidate.createMany({
    data: fresh.map((candidate, index) => ({
      code: `LC-${Date.now().toString(36).toUpperCase()}-${index + 1}`,
      kind: candidate.kind,
      title: candidate.title,
      titleAr: candidate.titleAr,
      reason: candidate.reason,
      reasonAr: candidate.reasonAr,
      evidence: candidate.evidence,
      evidenceAr: candidate.evidenceAr,
      eventCount: candidate.eventCount,
      confidence: candidate.confidence,
      supportPct: candidate.supportPct,
      proposedChange: candidate.proposedChange,
      proposedChangeAr: candidate.proposedChangeAr,
      status: 'new',
    })),
  })

  return fresh.length
}

export async function listLearningCandidates(
  options: { status?: string; limit?: number } = {},
): Promise<LearningCandidateView[]> {
  const rows = await prisma.learningCandidate.findMany({
    where: options.status ? { status: options.status } : undefined,
    orderBy: [{ status: 'asc' }, { confidence: 'desc' }],
    take: options.limit ?? 30,
  })

  return rows.map((row) => ({
    code: row.code,
    kind: row.kind as LearningCandidate['kind'],
    title: row.title,
    titleAr: row.titleAr,
    reason: row.reason,
    reasonAr: row.reasonAr,
    evidence: row.evidence,
    evidenceAr: row.evidenceAr,
    eventCount: row.eventCount,
    confidence: row.confidence,
    supportPct: row.supportPct,
    proposedChange: row.proposedChange,
    proposedChangeAr: row.proposedChangeAr,
    status: row.status,
    reviewedAt: row.reviewedAt?.getTime() ?? null,
    reviewNote: row.reviewNote,
    createdAt: row.createdAt.getTime(),
  }))
}

/**
 * Approve or reject a learning candidate.
 *
 * Approval records a human decision. It does **not** apply the change to a model,
 * threshold or weight — this build has no path from the learning queue to a deployed
 * model, which is the point of §32 rather than a limitation of it. The status is set to
 * `approved`, and applying it remains a separate, deliberate act.
 */
export async function reviewLearningCandidate(input: {
  code: string
  decision: 'approve' | 'reject'
  note?: string
  userId: string
}): Promise<boolean> {
  const candidate = await prisma.learningCandidate.findUnique({
    where: { code: input.code },
    select: { id: true, status: true },
  })
  if (!candidate) return false
  if (candidate.status === 'approved' || candidate.status === 'rejected') return false

  await prisma.learningCandidate.update({
    where: { id: candidate.id },
    data: {
      status: input.decision === 'approve' ? 'approved' : 'rejected',
      reviewedById: input.userId,
      reviewedAt: new Date(),
      reviewNote: input.note ?? '',
    },
  })

  return true
}

/** Memory rows for the /decisions timeline, with outcomes and lessons attached. */
export async function listDecisionMemory(options: { limit?: number; assetCode?: string } = {}) {
  const rows = await prisma.decisionMemory.findMany({
    where: options.assetCode ? { assetCode: options.assetCode } : undefined,
    orderBy: { createdAt: 'desc' },
    take: options.limit ?? 25,
    include: {
      outcomes: { orderBy: { createdAt: 'desc' }, take: 1 },
      lessons: { orderBy: { createdAt: 'desc' }, take: 3 },
    },
  })

  return rows.map((row) => ({
    code: row.code,
    assetCode: row.assetCode,
    title: row.title,
    titleAr: row.titleAr,
    strategyLabel: row.strategyLabel,
    strategyLabelAr: row.strategyLabelAr,
    riskBefore: row.riskBefore,
    riskAfter: row.riskAfter,
    rootCauseKey: row.rootCauseKey,
    dnaPatternCode: row.dnaPatternCode,
    evidenceKind: row.evidenceKind,
    outcomeStatus: row.outcomeStatus,
    createdAt: row.createdAt.getTime(),
    createdAtLabel: row.createdAt.toLocaleDateString(LOCALE),
    outcome: row.outcomes[0]
      ? {
          result: row.outcomes[0].result,
          observedRisk: row.outcomes[0].observedRisk,
          evidenceKind: row.outcomes[0].evidenceKind,
          notes: row.outcomes[0].notes,
          notesAr: row.outcomes[0].notesAr,
        }
      : null,
    lessons: row.lessons.map((lesson) => ({
      text: lesson.text,
      textAr: lesson.textAr,
      isApproved: lesson.isApproved,
    })),
  }))
}
