/**
 * The SCADA/EMS/DMS adapter registry and the live-readiness assessment (5.0 §8–§10).
 *
 * NABDH reads. It does not command.
 *
 * That sentence is the whole design of this file. Each adapter declares the protocol it
 * speaks, the point types it can read from that protocol, and — explicitly, as data rather
 * than as an omission — that it supports no write. The forbidden verbs are named so the
 * refusal is testable: a future contributor who adds `OPEN` to an adapter breaks a test
 * rather than quietly shipping a control path.
 *
 * The readiness half exists because §8 forbids calling anything LIVE without a real
 * endpoint behind it. Rather than a boolean, readiness is a checklist evaluated against
 * the connector's actual configuration, and a connector that fails any precondition is
 * reported as *not ready and why* — never as live-with-caveats.
 *
 * Pure functions over plain inputs. No database, no network, no clock beyond what is
 * passed in, so the rules here can be exercised without standing anything up.
 */

/** Every transport the platform has an adapter for. */
export const ADAPTER_PROTOCOLS = [
  'opcua',
  'modbus',
  'dnp3',
  'iec61850',
  'iec104',
  'mqtt',
  'rest',
  'websocket',
  'file',
] as const

export type AdapterProtocol = (typeof ADAPTER_PROTOCOLS)[number]

/** The quantities an adapter can read. Deliberately narrower than "anything". */
export const POINT_TYPES = [
  'analog_measurement',
  'digital_status',
  'accumulator',
  'event_log',
  'alarm',
  'metadata',
] as const

export type PointType = (typeof POINT_TYPES)[number]

/**
 * Control verbs NABDH does not implement, on any adapter.
 *
 * Listed rather than merely absent, so "does this platform command the grid?" has an
 * answer in the source and in a test, not in a promise. §32 names these exactly.
 */
export const FORBIDDEN_VERBS = [
  'OPEN',
  'CLOSE',
  'TRIP',
  'DISPATCH',
  'SWITCH',
  'CONTROL',
  'SETPOINT',
  'RESET',
] as const

export type ForbiddenVerb = (typeof FORBIDDEN_VERBS)[number]

export interface AdapterSpec {
  protocol: AdapterProtocol
  label: string
  labelAr: string
  /** What this transport is normally used for, so a mismatch is visible in the hub. */
  typicalSource: string
  reads: PointType[]
  /**
   * Always false. Present as a field rather than implied by absence: a reader has to see
   * it, and the day someone proposes a write path they have to change a declared value.
   */
  writeSupported: false
  /** Verbs this adapter refuses. The same list for every adapter, by design. */
  refuses: readonly ForbiddenVerb[]
  /** Whether the transport can push, or must be polled. Drives the readiness advice. */
  transport: 'push' | 'poll'
  /** What a real deployment must provide before this adapter can carry live decisions. */
  requires: Array<'endpoint' | 'credential' | 'certificate' | 'topic' | 'point_map'>
  /** Typical end-to-end latency, in seconds, for a correctly configured link. */
  expectedLatencySec: number
}

const READS_TELEMETRY: PointType[] = ['analog_measurement', 'digital_status', 'alarm']

/**
 * One entry per transport.
 *
 * The `requires` lists are what an integrator actually has to hand over — a certificate
 * for IEC 61850, a topic for MQTT, a point map for anything that speaks in register
 * numbers rather than names. A readiness report that asked for the same five things
 * regardless of protocol would be a form, not an assessment.
 */
export const ADAPTERS: Record<AdapterProtocol, AdapterSpec> = {
  opcua: {
    protocol: 'opcua',
    label: 'OPC UA',
    labelAr: 'OPC UA',
    typicalSource: 'SCADA historian and gateway',
    reads: [...READS_TELEMETRY, 'event_log', 'metadata'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'push',
    requires: ['endpoint', 'certificate', 'point_map'],
    expectedLatencySec: 2,
  },
  modbus: {
    protocol: 'modbus',
    label: 'Modbus TCP',
    labelAr: 'مودباس TCP',
    typicalSource: 'Field devices and meters',
    // Modbus carries registers, not names: without a point map the numbers are unlabelled.
    reads: ['analog_measurement', 'digital_status', 'accumulator'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'poll',
    requires: ['endpoint', 'point_map'],
    expectedLatencySec: 5,
  },
  dnp3: {
    protocol: 'dnp3',
    label: 'DNP3',
    labelAr: 'DNP3',
    typicalSource: 'Distribution RTUs',
    reads: [...READS_TELEMETRY, 'accumulator', 'event_log'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'push',
    requires: ['endpoint', 'point_map'],
    expectedLatencySec: 3,
  },
  iec61850: {
    protocol: 'iec61850',
    label: 'IEC 61850 MMS',
    labelAr: 'IEC 61850 MMS',
    typicalSource: 'Substation automation',
    reads: [...READS_TELEMETRY, 'event_log', 'metadata'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'push',
    requires: ['endpoint', 'certificate', 'point_map'],
    expectedLatencySec: 1,
  },
  iec104: {
    protocol: 'iec104',
    label: 'IEC 60870-5-104',
    labelAr: 'IEC 60870-5-104',
    typicalSource: 'Transmission control centre',
    reads: [...READS_TELEMETRY, 'event_log'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'push',
    requires: ['endpoint', 'point_map'],
    expectedLatencySec: 2,
  },
  mqtt: {
    protocol: 'mqtt',
    label: 'MQTT',
    labelAr: 'MQTT',
    typicalSource: 'IoT sensors and edge gateways',
    reads: ['analog_measurement', 'digital_status', 'metadata'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'push',
    requires: ['endpoint', 'credential', 'topic'],
    expectedLatencySec: 2,
  },
  rest: {
    protocol: 'rest',
    label: 'REST / JSON',
    labelAr: 'REST / JSON',
    typicalSource: 'Enterprise systems and weather services',
    reads: ['analog_measurement', 'accumulator', 'metadata'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'poll',
    requires: ['endpoint', 'credential'],
    expectedLatencySec: 30,
  },
  websocket: {
    protocol: 'websocket',
    label: 'WebSocket',
    labelAr: 'WebSocket',
    typicalSource: 'Streaming gateways',
    reads: ['analog_measurement', 'digital_status', 'alarm'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'push',
    requires: ['endpoint', 'credential'],
    expectedLatencySec: 1,
  },
  file: {
    protocol: 'file',
    label: 'File drop / CSV',
    labelAr: 'ملفات / CSV',
    typicalSource: 'Batch exports from asset registers',
    // A file is a batch. It can be a source of record; it cannot be a source of *live*.
    reads: ['metadata', 'accumulator', 'event_log'],
    writeSupported: false,
    refuses: FORBIDDEN_VERBS,
    transport: 'poll',
    requires: ['endpoint'],
    expectedLatencySec: 3_600,
  },
}

/** Resolve a stored protocol string, or null when nothing in the registry speaks it. */
export function adapterFor(protocol: string): AdapterSpec | null {
  return ADAPTERS[protocol as AdapterProtocol] ?? null
}

// ─────────────────────────────────────────────────────────────────────────────
// Live readiness (§9)
// ─────────────────────────────────────────────────────────────────────────────

export const READINESS_STATES = [
  /** Nothing is configured beyond a name. */
  'not_configured',
  /** Configured, but something required is still missing. */
  'incomplete',
  /** Everything required is present; the link has not been proven yet. */
  'ready_to_test',
  /** Tested and delivering, but still running against demonstration data. */
  'demo_verified',
  /** A real endpoint, tested, delivering, inside quality and latency limits. */
  'live_ready',
  /** Configured live and currently carrying data. */
  'live',
] as const

export type ReadinessState = (typeof READINESS_STATES)[number]

export interface ReadinessCheck {
  key:
    | 'adapter'
    | 'endpoint'
    | 'credential'
    | 'certificate'
    | 'topic'
    | 'point_map'
    | 'reachable'
    | 'delivering'
    | 'quality'
    | 'latency'
    | 'read_only'
  /** True when this precondition is satisfied by the configuration as it stands. */
  ok: boolean
  /** False for checks that are advisory rather than blocking. */
  required: boolean
  message: string
  messageAr: string
}

export interface ReadinessInput {
  protocol: string
  mode: 'live' | 'demo'
  status: string
  endpoint: string
  authKind: string
  secretRef: string
  mappings: number
  /** Records seen on the most recent sync. Zero means configured but silent. */
  recordsReceived: number
  lastSyncAt: number | null
  latencyMs: number
  dataQuality: number
  /** Now, passed in so this stays a pure function. */
  ts: number
}

export interface ReadinessReport {
  protocol: string
  adapter: AdapterSpec | null
  state: ReadinessState
  /** 0–100, the share of required checks satisfied. Never a confidence in the data. */
  completionPct: number
  checks: ReadinessCheck[]
  blocking: ReadinessCheck[]
  /** The single next thing to do, or null when there is nothing left to do. */
  nextStep: string | null
  nextStepAr: string | null
  /**
   * Whether this connector may be described as LIVE.
   *
   * §8, in one field: false unless a real endpoint is configured, the adapter exists, and
   * the link has actually delivered. A demonstration source is never live, however healthy.
   */
  canClaimLive: boolean
  note: string
  noteAr: string
}

/** Data older than this has stopped being live, whatever the connector's status says. */
const STALE_AFTER_MIN = 15

/** Below this, a source is delivering numbers nobody should decide on. */
const QUALITY_FLOOR = 60

/**
 * Assess one connector against what a live link actually requires.
 *
 * Every check reads a real field. Nothing here is scored on intent: a connector with
 * `mode: 'live'` and no endpoint fails the endpoint check exactly as loudly as one that
 * never claimed anything.
 */
export function assessLiveReadiness(input: ReadinessInput): ReadinessReport {
  const adapter = adapterFor(input.protocol)
  const checks: ReadinessCheck[] = []

  const add = (
    key: ReadinessCheck['key'],
    ok: boolean,
    required: boolean,
    message: string,
    messageAr: string,
  ) => checks.push({ key, ok, required, message, messageAr })

  add(
    'adapter',
    Boolean(adapter),
    true,
    adapter
      ? `${adapter.label} adapter available, read-only.`
      : `No adapter in this platform speaks "${input.protocol}".`,
    adapter ? `محوّل ${adapter.label} متاح، للقراءة فقط.` : `لا يوجد محوّل يتحدث "${input.protocol}".`,
  )

  const needs = new Set(adapter?.requires ?? ['endpoint'])

  add(
    'endpoint',
    input.endpoint.trim().length > 0,
    needs.has('endpoint'),
    input.endpoint.trim().length > 0
      ? 'An endpoint is configured.'
      : 'No endpoint. Nothing can be read until one is configured.',
    input.endpoint.trim().length > 0 ? 'نقطة الاتصال مهيأة.' : 'لا توجد نقطة اتصال. لا يمكن قراءة أي شيء قبل تهيئتها.',
  )

  // The credential itself never appears here — only whether one has been named. §32 keeps
  // secrets out of every payload this platform produces, including this report.
  const hasCredential = input.authKind !== 'none' && input.secretRef.trim().length > 0
  add(
    'credential',
    hasCredential,
    needs.has('credential'),
    hasCredential
      ? `A ${input.authKind} credential is referenced by name: ${input.secretRef}.`
      : 'No credential is referenced. The value is never stored here — only which environment variable holds it.',
    hasCredential
      ? `يُشار إلى بيانات اعتماد من نوع ${input.authKind} بالاسم: ${input.secretRef}.`
      : 'لا يوجد مرجع لبيانات الاعتماد.',
  )

  if (needs.has('certificate')) {
    const hasCertificate = input.authKind === 'certificate' && input.secretRef.trim().length > 0
    add(
      'certificate',
      hasCertificate,
      true,
      hasCertificate
        ? 'A certificate reference is configured.'
        : 'This protocol requires a certificate, and none is referenced.',
      hasCertificate ? 'مرجع الشهادة مهيأ.' : 'هذا البروتوكول يتطلب شهادة، ولا يوجد مرجع لها.',
    )
  }

  if (needs.has('topic')) {
    // A topic is carried as a path on the endpoint for this platform's connectors. The
    // scheme's own `//` is not a path — checking for a bare slash would pass every broker
    // address and make this row decorative.
    const afterScheme = input.endpoint.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '')
    const hasTopic = afterScheme.includes('/') || afterScheme.includes('#')
    add(
      'topic',
      hasTopic,
      true,
      hasTopic ? 'A topic or topic pattern is present on the endpoint.' : 'No topic is subscribed.',
      hasTopic ? 'يوجد موضوع اشتراك على نقطة الاتصال.' : 'لا يوجد موضوع مشترك فيه.',
    )
  }

  if (needs.has('point_map')) {
    add(
      'point_map',
      input.mappings > 0,
      true,
      input.mappings > 0
        ? `${input.mappings} fields mapped onto the model.`
        : 'This protocol speaks in registers or node ids. Without a point map the values arrive unlabelled.',
      input.mappings > 0 ? `${input.mappings} حقلًا مرتبطًا بالنموذج.` : 'بدون خريطة نقاط تصل القيم بلا تسمية.',
    )
  }

  const reachable = input.status === 'connected' || input.status === 'degraded'
  add(
    'reachable',
    reachable,
    true,
    reachable ? 'The last attempt reached the source.' : `Status is ${input.status}.`,
    reachable ? 'المحاولة الأخيرة وصلت إلى المصدر.' : `الحالة: ${input.status}.`,
  )

  const ageMin = input.lastSyncAt === null ? Number.POSITIVE_INFINITY : (input.ts - input.lastSyncAt) / 60_000
  const delivering = input.recordsReceived > 0 && ageMin <= STALE_AFTER_MIN
  add(
    'delivering',
    delivering,
    true,
    delivering
      ? `Delivering; last record ${Math.round(ageMin)} min ago.`
      : input.recordsReceived === 0
        ? 'Configured but silent — no records have arrived.'
        : `Last record ${Number.isFinite(ageMin) ? Math.round(ageMin) : '∞'} min ago, past the ${STALE_AFTER_MIN} min limit.`,
    delivering ? 'يصل البيانات بانتظام.' : 'مهيأ لكنه صامت أو متأخر.',
  )

  add(
    'quality',
    input.dataQuality >= QUALITY_FLOOR,
    false,
    input.dataQuality >= QUALITY_FLOOR
      ? `Data quality ${Math.round(input.dataQuality)}.`
      : `Data quality ${Math.round(input.dataQuality)}, below the ${QUALITY_FLOOR} floor for decisions.`,
    `جودة البيانات ${Math.round(input.dataQuality)}.`,
  )

  const latencyOk = adapter ? input.latencyMs <= adapter.expectedLatencySec * 1_000 * 3 : true
  add(
    'latency',
    latencyOk,
    false,
    latencyOk
      ? `Latency ${Math.round(input.latencyMs)} ms.`
      : `Latency ${Math.round(input.latencyMs)} ms, well past what this protocol should take.`,
    `زمن الاستجابة ${Math.round(input.latencyMs)} مللي ثانية.`,
  )

  // Always satisfied, and always shown. The point of the row is that an operator reading
  // the readiness list sees the read-only guarantee stated, not inferred from silence.
  add(
    'read_only',
    true,
    true,
    'Read-only. This adapter implements no control verb; NABDH sends no commands to SCADA, EMS or DMS.',
    'للقراءة فقط. لا ينفذ هذا المحوّل أي أمر تحكم، ولا يرسل نبض أوامر إلى SCADA أو EMS أو DMS.',
  )

  const required = checks.filter((check) => check.required)
  const satisfied = required.filter((check) => check.ok)
  const blocking = required.filter((check) => !check.ok)
  // Delivery is a state of the link, not a gap in its configuration. Counting it as a
  // configuration gap would collapse "configured but never tested" into "incomplete" and
  // lose the distinction the readiness ladder exists to make.
  const configGaps = blocking.filter((check) => check.key !== 'delivering')

  const hasEndpoint = checks.find((check) => check.key === 'endpoint')?.ok ?? false
  const canClaimLive = input.mode === 'live' && hasEndpoint && Boolean(adapter) && delivering

  const state: ReadinessState = !adapter || !hasEndpoint
    ? input.endpoint.trim().length === 0 && input.mappings === 0
      ? 'not_configured'
      : 'incomplete'
    : configGaps.length > 0
      ? 'incomplete'
      : !delivering
        ? 'ready_to_test'
        : input.mode === 'demo'
          ? 'demo_verified'
          : canClaimLive
            ? 'live'
            : 'live_ready'

  const next = blocking[0] ?? null

  return {
    protocol: input.protocol,
    adapter,
    state,
    completionPct: required.length === 0 ? 0 : Math.round((satisfied.length / required.length) * 100),
    checks,
    blocking,
    nextStep: next?.message ?? null,
    nextStepAr: next?.messageAr ?? null,
    canClaimLive,
    note:
      state === 'demo_verified'
        ? 'Verified against demonstration data. A demonstration source is never live, however healthy it looks.'
        : canClaimLive
          ? 'A real endpoint, reachable and delivering. This connector may be described as live.'
          : 'Not live. Every reading from this connector is labelled by its actual source, not by its intent.',
    noteAr:
      state === 'demo_verified'
        ? 'تم التحقق مقابل بيانات عرض. مصدر العرض ليس مصدرًا حيًا مهما بدت صحته.'
        : canClaimLive
          ? 'نقطة اتصال حقيقية تصل وتُسلّم البيانات. يمكن وصف هذا الموصل بأنه حي.'
          : 'ليس حيًا. كل قراءة من هذا الموصل تُوسم بمصدرها الفعلي لا بالنية.',
  }
}
