import 'server-only'

import { prisma } from '../db'
import { assetAgeYears, maintenanceDebt } from '../engine/grid'
import {
  DEFAULT_CONSTRAINTS,
  assetEnvelope,
  validatePhysics,
  type OperatingEnvelope,
  type PhysicsConstraintDef,
  type PhysicsValidation,
  type ValidationSubject,
} from '../engine/physics'
import { sampleAsset, type AppliedAction, type Perturbation } from '../engine/telemetry'
import { environmentFor } from '../engine/grid'
import { getRegion } from '../data/regions'
import { getGridSnapshot } from './grid-service'
import { resourcesFor } from '../engine/grid'
import { round } from '../engine/rng'

/**
 * The physics layer's server side (§10, §11, §12, §13).
 *
 * Two responsibilities, kept apart on purpose:
 *
 *   - **Validation** answers "may this recommendation be shown to a human at all?" It is
 *     called before a strategy reaches the UI, and it writes an auditable record of what
 *     was checked, so a rejection can be defended after the fact.
 *
 *   - **Envelopes** answer "where is this asset allowed to operate today?" They are
 *     recomputed from live conditions rather than read from a table, because a ceiling
 *     that does not move with the weather is not an engineering limit, it is a caption.
 *
 * The constraint set is loaded from the database when present and falls back to the
 * definitions compiled into the engine. That order matters: an operator must be able to
 * see and change the limits, but the platform must never silently run with *no* limits
 * because a table was empty.
 */

let constraintCache: { at: number; defs: PhysicsConstraintDef[] } | null = null
const CONSTRAINT_TTL_MS = 60_000

/** Every active constraint, database first, engine defaults as the floor. */
export async function loadConstraints(): Promise<PhysicsConstraintDef[]> {
  const now = Date.now()
  if (constraintCache && now - constraintCache.at < CONSTRAINT_TTL_MS) return constraintCache.defs

  const rows = await prisma.physicsConstraint.findMany({
    where: { isActive: true },
    orderBy: { code: 'asc' },
  })

  const defs: PhysicsConstraintDef[] = rows.length
    ? rows.map((row) => ({
        code: row.code,
        kind: row.kind as PhysicsConstraintDef['kind'],
        scopeKind: row.scopeKind,
        scopeCode: row.scopeCode,
        label: row.name,
        labelAr: row.nameAr,
        comparator: row.comparator === 'gte' ? 'gte' : 'lte',
        limitValue: row.limitValue,
        warnValue: row.warnValue,
        unit: row.unit,
        severity: row.severity === 'soft' ? 'soft' : 'hard',
      }))
    : DEFAULT_CONSTRAINTS

  constraintCache = { at: now, defs }
  return defs
}

/** Drop the cache after an administrative change. */
export function invalidateConstraintCache(): void {
  constraintCache = null
}

export interface ValidationRequest {
  assetCode: string
  actions: AppliedAction[]
  ts?: number
  perturbation?: Perturbation
  subjectKind?: string
  subjectId?: string
  /** Persist the run so it can be produced as evidence later. */
  persist?: boolean
  userId?: string | null
}

export interface ValidationOutcome extends PhysicsValidation {
  assetCode: string
  runCode: string | null
  envelope: OperatingEnvelope
}

/**
 * Validate a proposed set of actions against the engineering limits.
 *
 * The end state is computed by re-sampling the asset with the actions applied, which is
 * the same path the simulation takes. Validating against an estimate of the end state
 * rather than the modelled one would let a strategy pass the validator and then fail in
 * simulation, which is the worst of both.
 */
export async function validateActions(request: ValidationRequest): Promise<ValidationOutcome | null> {
  const ts = request.ts ?? Date.now()
  const perturbation = request.perturbation ?? {}

  const [snapshot, constraints] = await Promise.all([
    getGridSnapshot({ ts, perturbation }),
    loadConstraints(),
  ])

  const focus = snapshot.assets.find((entry) => entry.profile.code === request.assetCode)
  if (!focus) return null

  const region = getRegion(focus.profile.regionCode)
  const env = environmentFor(region, perturbation)(ts)
  const resources = resourcesFor(focus.profile, snapshot.assets)
  const regionSnapshot = snapshot.regions.find((entry) => entry.code === region.code)

  const after = sampleAsset(focus.profile, ts, env, { ...perturbation, actions: request.actions })

  const subject: ValidationSubject = {
    profile: focus.profile,
    after,
    actions: request.actions,
    resources: {
      transferCapacityMw: resources.transferCapacityMw,
      batteryPowerMw: resources.batteryPowerMw,
      flexibleLoadMw: resources.flexibleLoadMw,
    },
    reserveMarginPct: regionSnapshot?.reserveMarginPct ?? snapshot.reserveMarginPct,
    frequencyDeviationHz: Math.abs(snapshot.frequencyHz - 60),
    unavailableTargets: request.actions
      .map((action) => action.assetCode)
      .filter((code) => {
        const target = snapshot.assets.find((entry) => entry.profile.code === code)
        return Boolean(target && (target.sample.isOutage || target.profile.status !== 'online'))
      }),
    constraints,
  }

  const validation = validatePhysics(subject)

  const envelope = assetEnvelope({
    profile: focus.profile,
    sample: focus.sample,
    ageYears: assetAgeYears(focus.profile, ts),
    maintenanceDebt: Math.min(1, maintenanceDebt(focus.profile, ts) / 365),
  })

  let runCode: string | null = null
  if (request.persist) {
    runCode = `PV-${Date.now().toString(36).toUpperCase()}`
    const byCode = new Map(
      (await prisma.physicsConstraint.findMany({ select: { id: true, code: true } })).map((row) => [
        row.code,
        row.id,
      ]),
    )

    await prisma.physicsValidationRun.create({
      data: {
        code: runCode,
        subjectKind: request.subjectKind ?? 'strategy',
        subjectId: request.subjectId ?? request.assetCode,
        assetCode: request.assetCode,
        verdict: validation.verdict,
        checksRun: validation.checks.length,
        checksFailed: validation.failed.length,
        warnings: validation.warnings.length,
        summary: validation.summary,
        summaryAr: validation.summaryAr,
        runById: request.userId ?? null,
        checks: {
          create: validation.checks.map((check) => ({
            constraintId: byCode.get(check.constraintCode) ?? null,
            constraintCode: check.constraintCode,
            assetCode: check.assetCode,
            label: check.label,
            labelAr: check.labelAr,
            observedValue: check.observedValue,
            limitValue: check.limitValue,
            unit: check.unit,
            headroom: check.headroom,
            passed: check.passed,
            severity: check.severity,
            reason: check.reason,
            reasonAr: check.reasonAr,
          })),
        },
      },
    })
  }

  return { ...validation, assetCode: request.assetCode, runCode, envelope }
}

/**
 * The operating envelope for one asset under current conditions.
 *
 * Persisted opportunistically so /data-health and the twin can show how a ceiling has
 * moved over the day without recomputing the whole fleet on every render.
 */
export async function getEnvelope(
  assetCode: string,
  options: { ts?: number; perturbation?: Perturbation; persist?: boolean } = {},
): Promise<OperatingEnvelope | null> {
  const ts = options.ts ?? Date.now()
  const snapshot = await getGridSnapshot({ ts, perturbation: options.perturbation })
  const focus = snapshot.assets.find((entry) => entry.profile.code === assetCode)
  if (!focus) return null

  const envelope = assetEnvelope({
    profile: focus.profile,
    sample: focus.sample,
    ageYears: assetAgeYears(focus.profile, ts),
    maintenanceDebt: Math.min(1, maintenanceDebt(focus.profile, ts) / 365),
  })

  if (options.persist) await persistEnvelope(envelope)
  return envelope
}

/** Envelopes for the assets under the most pressure, for the fleet view. */
export async function getFleetEnvelopes(
  options: { ts?: number; perturbation?: Perturbation; limit?: number } = {},
): Promise<OperatingEnvelope[]> {
  const ts = options.ts ?? Date.now()
  const limit = options.limit ?? 12
  const snapshot = await getGridSnapshot({ ts, perturbation: options.perturbation })

  return snapshot.assets
    .filter((entry) => entry.profile.capacityMw > 0 && !entry.sample.isOutage)
    .map((entry) =>
      assetEnvelope({
        profile: entry.profile,
        sample: entry.sample,
        ageYears: assetAgeYears(entry.profile, ts),
        maintenanceDebt: Math.min(1, maintenanceDebt(entry.profile, ts) / 365),
      }),
    )
    // Least headroom first: the fleet view exists to answer "what is closest to its
    // ceiling", not "what is biggest".
    .sort((a, b) => a.headroomPct - b.headroomPct)
    .slice(0, limit)
}

async function persistEnvelope(envelope: OperatingEnvelope): Promise<void> {
  const penalty = (key: string) =>
    round(envelope.penalties.find((p) => p.key === key)?.points ?? 0, 2)

  await prisma.assetOperatingEnvelope.create({
    data: {
      assetCode: envelope.assetCode,
      currentPct: envelope.currentPct,
      normalMaxPct: envelope.normalMaxPct,
      recommendedMaxPct: envelope.recommendedMaxPct,
      warningPct: envelope.warningPct,
      criticalPct: envelope.criticalPct,
      physicalLimitPct: envelope.physicalLimitPct,
      adjustedMaxPct: envelope.adjustedMaxPct,
      ambientC: envelope.ambientC,
      heatPenaltyPct: penalty('heat'),
      agePenaltyPct: penalty('age'),
      maintenancePenaltyPct: penalty('maintenance'),
      coolingPenaltyPct: penalty('cooling'),
      band: envelope.band,
      headroomPct: envelope.headroomPct,
      source: 'demo_model',
    },
  })
}

/** Recent validation runs, for the governance and evidence views. */
export async function listValidationRuns(options: { limit?: number; assetCode?: string } = {}) {
  return prisma.physicsValidationRun.findMany({
    where: options.assetCode ? { assetCode: options.assetCode } : undefined,
    orderBy: { createdAt: 'desc' },
    take: options.limit ?? 20,
    include: { checks: { orderBy: { passed: 'asc' } } },
  })
}
