import type { PrismaClient } from '@prisma/client'

import {
  assessSourceQuality,
  connectorHealth,
  type ConnectorStatus,
} from '../../src/lib/engine/integration'
import { round, seededRange } from '../../src/lib/engine/rng'

/**
 * The demonstration connectors (4.1, Integration Hub).
 *
 * Every connector seeded here is `mode: 'demo'`. That is not a placeholder standing in
 * for a real integration that was never finished — it is the honest description of what
 * these are: generators that speak the shape and the units a real SCADA, weather, IoT,
 * CMMS, historian or forecast feed would send. The hub renders `demo_connected`, never
 * `connected`, because the label is derived from mode and status together.
 *
 * The mappings are deliberately *not* identity mappings. The sources emit Fahrenheit,
 * kilowatts and volts; the platform stores Celsius, megawatts and kilovolts. A hub whose
 * mapping layer converted nothing would be demonstrating a mapping layer that does
 * nothing.
 *
 * The health history is computed by the same two functions the live sync path uses, from
 * record counts that vary per point. It is not a hand-drawn sparkline: change the quality
 * weights and every number here moves.
 *
 * No credential appears anywhere below. `secretRef` names an environment variable so an
 * operator can see which secret a connector *would* need; there is no column, and no code
 * path, that could carry the value.
 */

const MIN = 60_000

interface SeedContext {
  prisma: PrismaClient
  now: number
}

interface MappingSpec {
  sourceField: string
  targetField: string
  dataType?: string
  sourceUnit?: string
  targetUnit?: string
  minValue?: number
  maxValue?: number
  isRequired?: boolean
  notes?: string
}

interface ConnectorSpec {
  code: string
  name: string
  nameAr: string
  kind: string
  protocol: string
  direction: string
  description: string
  descriptionAr: string
  endpoint: string
  authKind: string
  secretRef: string
  pollSeconds: number
  timeoutMs: number
  status: ConnectorStatus
  /** Fraction of the expected batch this source actually delivers, per sample. */
  delivery: number
  /** Fraction of what it delivers that survives validation. */
  cleanliness: number
  baseLatencyMs: number
  seed: number
  mappings: MappingSpec[]
}

/** The two identity-carrying fields every inbound source must provide. */
const IDENTITY: MappingSpec[] = [
  {
    sourceField: 'asset_id',
    targetField: 'assetCode',
    dataType: 'string',
    isRequired: true,
    notes: 'A record whose asset is unknown is discarded, never attached to a guess.',
  },
  {
    sourceField: 'timestamp',
    targetField: 'ts',
    dataType: 'timestamp',
    isRequired: true,
    notes: 'Used for freshness scoring and duplicate detection.',
  },
]

const CONNECTORS: ConnectorSpec[] = [
  {
    code: 'CONN-SCADA-CENTRAL',
    name: 'Demo SCADA — Central Region',
    nameAr: 'سكادا تجريبية — المنطقة الوسطى',
    kind: 'scada',
    protocol: 'opcua',
    direction: 'inbound',
    description:
      'Simulated substation telemetry in the shape and units a Central-region SCADA gateway sends: power in kilowatts, temperature in Fahrenheit, voltage in volts.',
    descriptionAr:
      'قياسات محطات محاكاة بنفس شكل ووحدات بوابة سكادا في المنطقة الوسطى: القدرة بالكيلوواط، والحرارة بالفهرنهايت، والجهد بالفولت.',
    endpoint: 'opc.tcp://demo-scada.internal:4840/nabdh',
    authKind: 'certificate',
    secretRef: 'NABDH_SCADA_CLIENT_CERT',
    pollSeconds: 5,
    timeoutMs: 3_000,
    status: 'connected',
    delivery: 0.99,
    cleanliness: 0.985,
    baseLatencyMs: 42,
    seed: 8_101,
    mappings: [
      ...IDENTITY,
      { sourceField: 'active_power', targetField: 'loadMw', sourceUnit: 'kW', targetUnit: 'MW', minValue: 0, maxValue: 5_000, isRequired: true },
      { sourceField: 'transformer_temperature', targetField: 'tempC', sourceUnit: 'F', targetUnit: 'C', minValue: -20, maxValue: 200, isRequired: true },
      { sourceField: 'bus_voltage', targetField: 'voltageKv', sourceUnit: 'V', targetUnit: 'kV', minValue: 0, maxValue: 800 },
      { sourceField: 'line_current', targetField: 'currentA', sourceUnit: 'A', targetUnit: 'A', minValue: 0, maxValue: 6_000 },
      { sourceField: 'loading_pct', targetField: 'loadPct', sourceUnit: 'pct', targetUnit: '%', minValue: 0, maxValue: 200 },
    ],
  },
  {
    code: 'CONN-SCADA-EAST',
    name: 'Demo SCADA — Eastern Region',
    nameAr: 'سكادا تجريبية — المنطقة الشرقية',
    kind: 'scada',
    protocol: 'modbus',
    direction: 'inbound',
    description:
      'A second simulated SCADA gateway, polled less often and over a slower link, so the hub has two sources of the same kind whose quality can be compared.',
    descriptionAr:
      'بوابة سكادا محاكاة ثانية، استطلاع أبطأ ووصلة أبطأ، لتوفير مصدرين من النوع نفسه يمكن مقارنة جودتهما.',
    endpoint: 'modbus://demo-scada-east.internal:502',
    authKind: 'none',
    secretRef: '',
    pollSeconds: 30,
    timeoutMs: 6_000,
    status: 'degraded',
    delivery: 0.88,
    cleanliness: 0.94,
    baseLatencyMs: 310,
    seed: 8_233,
    mappings: [
      ...IDENTITY,
      { sourceField: 'active_power', targetField: 'loadMw', sourceUnit: 'kW', targetUnit: 'MW', minValue: 0, maxValue: 5_000, isRequired: true },
      { sourceField: 'transformer_temperature', targetField: 'tempC', sourceUnit: 'F', targetUnit: 'C', minValue: -20, maxValue: 200, isRequired: true },
      { sourceField: 'bus_voltage', targetField: 'voltageKv', sourceUnit: 'V', targetUnit: 'kV', minValue: 0, maxValue: 800 },
      { sourceField: 'loading_pct', targetField: 'loadPct', sourceUnit: 'pct', targetUnit: '%', minValue: 0, maxValue: 200 },
    ],
  },
  {
    code: 'CONN-WEATHER',
    name: 'Demo Weather Service',
    nameAr: 'خدمة أرصاد تجريبية',
    kind: 'weather',
    protocol: 'rest',
    direction: 'inbound',
    description:
      'Ambient conditions per site. Reports hourly, so it is not stale at fifty minutes — freshness is scored against this cadence rather than a fixed threshold.',
    descriptionAr:
      'الظروف المحيطة لكل موقع. يرسل كل ساعة، لذا لا يُعد قديمًا عند خمسين دقيقة — تُقاس الحداثة بحسب هذه الوتيرة لا بعتبة ثابتة.',
    endpoint: 'https://demo-weather.internal/v1/observations',
    authKind: 'api_key',
    secretRef: 'NABDH_WEATHER_API_KEY',
    pollSeconds: 3_600,
    timeoutMs: 8_000,
    status: 'connected',
    delivery: 0.97,
    cleanliness: 0.99,
    baseLatencyMs: 480,
    seed: 8_317,
    mappings: [
      ...IDENTITY,
      { sourceField: 'ambient_temp_f', targetField: 'ambientC', sourceUnit: 'F', targetUnit: 'C', minValue: -30, maxValue: 70, isRequired: true },
      { sourceField: 'humidity_pct', targetField: 'humidityPct', sourceUnit: 'pct', targetUnit: '%', minValue: 0, maxValue: 100 },
    ],
  },
  {
    code: 'CONN-IOT-SENSORS',
    name: 'Demo IoT Sensor Mesh',
    nameAr: 'شبكة مستشعرات إنترنت الأشياء التجريبية',
    kind: 'iot',
    protocol: 'mqtt',
    direction: 'inbound',
    description:
      'Retrofitted transformer sensors publishing over MQTT. The noisiest source in the hub, and the one whose rejection log is worth reading.',
    descriptionAr:
      'مستشعرات محولات مضافة لاحقًا تنشر عبر MQTT. أكثر المصادر ضجيجًا، وسجل الرفض الخاص به هو الأجدر بالقراءة.',
    endpoint: 'mqtts://demo-iot.internal:8883/nabdh/+/telemetry',
    authKind: 'basic',
    secretRef: 'NABDH_IOT_BROKER_PASSWORD',
    pollSeconds: 10,
    timeoutMs: 4_000,
    status: 'connected',
    delivery: 0.93,
    cleanliness: 0.91,
    baseLatencyMs: 95,
    seed: 8_419,
    mappings: [
      ...IDENTITY,
      { sourceField: 'transformer_temperature', targetField: 'tempC', sourceUnit: 'F', targetUnit: 'C', minValue: -20, maxValue: 200, isRequired: true },
      { sourceField: 'vibration_mm_s', targetField: 'vibrationMmS', targetUnit: 'mm/s', minValue: 0, maxValue: 25 },
    ],
  },
  {
    code: 'CONN-CMMS',
    name: 'Demo Maintenance System (CMMS)',
    nameAr: 'نظام الصيانة التجريبي',
    kind: 'cmms',
    protocol: 'rest',
    direction: 'bidirectional',
    description:
      'Work-order state and service history. Bidirectional in configuration only: this build never writes a work order, because raising one is an operational act.',
    descriptionAr:
      'حالة أوامر العمل وسجل الخدمة. ثنائي الاتجاه في الإعداد فقط: لا يكتب هذا الإصدار أي أمر عمل، لأن إصداره إجراء تشغيلي.',
    endpoint: 'https://demo-cmms.internal/api/v2',
    authKind: 'oauth2',
    secretRef: 'NABDH_CMMS_CLIENT_SECRET',
    pollSeconds: 900,
    timeoutMs: 10_000,
    status: 'connected',
    delivery: 1,
    cleanliness: 1,
    baseLatencyMs: 620,
    seed: 8_527,
    mappings: [
      ...IDENTITY,
      { sourceField: 'work_order_open', targetField: 'hasOpenWorkOrder', dataType: 'boolean' },
      { sourceField: 'last_service_days', targetField: 'daysSinceService', minValue: 0, maxValue: 4_000 },
    ],
  },
  {
    code: 'CONN-RENEWABLE',
    name: 'Demo Renewable Forecast',
    nameAr: 'تنبؤ الطاقة المتجددة التجريبي',
    kind: 'historian',
    protocol: 'rest',
    direction: 'inbound',
    description:
      'Day-ahead solar and wind availability. Currently disconnected — the hub shows a source that is not delivering rather than hiding it inside an average.',
    descriptionAr:
      'توافر الطاقة الشمسية وطاقة الرياح لليوم التالي. غير متصل حاليًا — تعرض المنصة مصدرًا لا يرسل بدلًا من إخفائه داخل متوسط.',
    endpoint: 'https://demo-renewables.internal/v1/day-ahead',
    authKind: 'api_key',
    secretRef: 'NABDH_RENEWABLE_API_KEY',
    pollSeconds: 1_800,
    timeoutMs: 12_000,
    status: 'disconnected',
    delivery: 0,
    cleanliness: 0,
    baseLatencyMs: 0,
    seed: 8_631,
    mappings: [
      ...IDENTITY,
      { sourceField: 'active_power', targetField: 'availableMw', sourceUnit: 'kW', targetUnit: 'MW', minValue: 0, maxValue: 9_000 },
    ],
  },
]

export async function resetIntegrations(prisma: PrismaClient): Promise<void> {
  // Cascades handle mappings, events and samples; deleting the parent is enough and keeps
  // the order-of-deletion knowledge in one place — the schema.
  await prisma.integrationConnector.deleteMany({})
}

export async function seedIntegrations({ prisma, now }: SeedContext): Promise<number> {
  await resetIntegrations(prisma)

  for (const spec of CONNECTORS) {
    const history = buildHistory(spec, now)
    const latest = history[history.length - 1]
    const offline = spec.status === 'disconnected'

    const connector = await prisma.integrationConnector.create({
      data: {
        code: spec.code,
        name: spec.name,
        nameAr: spec.nameAr,
        kind: spec.kind,
        protocol: spec.protocol,
        mode: 'demo',
        status: spec.status,
        direction: spec.direction,
        description: spec.description,
        descriptionAr: spec.descriptionAr,
        endpoint: spec.endpoint,
        authKind: spec.authKind,
        secretRef: spec.secretRef,
        pollSeconds: spec.pollSeconds,
        timeoutMs: spec.timeoutMs,
        lastConnectedAt: new Date(now - (offline ? 214 : 1) * MIN),
        lastSyncAt: new Date(now - (offline ? 214 : 1) * MIN),
        latencyMs: latest.latencyMs,
        recordsReceived: history.reduce((sum, point) => sum + point.received, 0),
        errorCount: history.reduce((sum, point) => sum + point.rejected, 0),
        healthScore: latest.health,
        dataQuality: latest.quality.score,
        eventsPerMin: round(latest.received / Math.max(0.1, spec.pollSeconds / 60), 1),
        faultKind: '',
        isActive: true,
        mappings: {
          create: spec.mappings.map((mapping) => ({
            sourceField: mapping.sourceField,
            targetField: mapping.targetField,
            dataType: mapping.dataType ?? 'number',
            sourceUnit: mapping.sourceUnit ?? '',
            targetUnit: mapping.targetUnit ?? '',
            transform:
              mapping.sourceUnit && mapping.sourceUnit !== mapping.targetUnit ? 'convert' : 'none',
            minValue: mapping.minValue ?? null,
            maxValue: mapping.maxValue ?? null,
            isRequired: mapping.isRequired ?? false,
            notes: mapping.notes ?? '',
          })),
        },
      },
    })

    await prisma.integrationHealthSample.createMany({
      data: history.map((point) => ({
        connectorId: connector.id,
        healthScore: point.health,
        dataQuality: point.quality.score,
        latencyMs: point.latencyMs,
        completeness: point.quality.completeness,
        freshness: point.quality.freshness,
        validity: point.quality.validity,
        consistency: point.quality.consistency,
        availability: point.quality.availability,
        trust: point.quality.trust,
        ts: new Date(point.ts),
      })),
    })

    await prisma.integrationEvent.createMany({
      data: history.slice(-8).map((point) => ({
        connectorId: connector.id,
        kind: point.received === 0 ? 'error' : point.rejected > 0 ? 'degraded' : 'sync',
        recordsIn: point.received,
        recordsAccepted: point.received - point.rejected,
        recordsRejected: point.rejected,
        latencyMs: point.latencyMs,
        detail:
          point.received === 0
            ? 'No response from the source within the configured timeout'
            : `${point.received - point.rejected} of ${point.received} records accepted from the demo source`,
        detailAr:
          point.received === 0
            ? 'لا استجابة من المصدر خلال المهلة المحددة'
            : `قُبل ${point.received - point.rejected} من ${point.received} سجلًا من المصدر التجريبي`,
        severity: point.received === 0 ? 'error' : point.rejected > 0 ? 'warning' : 'info',
        ts: new Date(point.ts),
      })),
    })
  }

  return CONNECTORS.length
}

interface HistoryPoint {
  ts: number
  received: number
  rejected: number
  latencyMs: number
  quality: ReturnType<typeof assessSourceQuality>
  health: number
}

/**
 * Twenty-four points of history per connector, one every five minutes.
 *
 * Counts vary a little per point so the sparkline has texture, but the quality and health
 * numbers are never written directly — they are whatever the two engine functions return
 * for those counts. That is the difference between a chart of a health model and a chart
 * shaped to look like one.
 */
function buildHistory(spec: ConnectorSpec, now: number): HistoryPoint[] {
  const points: HistoryPoint[] = []
  const expected = 24

  for (let index = 23; index >= 0; index -= 1) {
    const ts = now - index * 5 * MIN

    const delivery = clamp01(spec.delivery + seededRange(spec.seed, index, -0.03, 0.02))
    const received = Math.round(expected * delivery)
    const cleanliness = clamp01(spec.cleanliness + seededRange(spec.seed, index + 40, -0.02, 0.015))
    const rejected = Math.round(received * (1 - cleanliness))
    const accepted = Math.max(0, received - rejected)

    const latencyMs = round(spec.baseLatencyMs * (1 + seededRange(spec.seed, index + 80, -0.15, 0.25)), 1)

    const quality = assessSourceQuality({
      expectedRecords: expected,
      receivedRecords: received,
      acceptedRecords: accepted,
      // Half a polling interval of age on average, which is what a source polled on a
      // fixed cadence actually presents.
      ageMin: spec.pollSeconds / 120,
      expectedIntervalMin: Math.max(0.1, spec.pollSeconds / 60),
      uptimeRatio: spec.status === 'disconnected' ? 0 : 1,
      agreementRatio: received ? accepted / received : 0,
      mode: 'demo',
    })

    points.push({
      ts,
      received,
      rejected,
      latencyMs,
      quality,
      health: connectorHealth({
        status: spec.status,
        quality: quality.score,
        latencyMs,
        timeoutMs: spec.timeoutMs,
        errorCount: rejected,
        recordsReceived: received,
      }),
    })
  }

  return points
}

function clamp01(value: number): number {
  return Math.min(1, Math.max(0, value))
}
