/**
 * Natural-language scenario parsing (§58).
 *
 * "حاكي ارتفاع الحمل في شمال الرياض 25% مع انخفاض الطاقة الشمسية 20% لمدة ساعتين" has to
 * become a structured scenario the engine can run. This does that with grammar rather
 * than with a model: a small vocabulary of quantities, directions and subjects, matched
 * in both languages.
 *
 * The choice is deliberate and has the same justification as the rest of the platform's
 * "AI": the result is inspectable, reproducible, works with no network, and — most
 * importantly — cannot hallucinate a megawatt figure the operator never typed. What it
 * cannot parse, it says it cannot parse, and the caller is shown a preview to correct
 * before anything runs.
 */

import { clamp, round } from './rng'

export interface ParsedEvent {
  kind:
    | 'heat_wave'
    | 'load_spike'
    | 'solar_drop'
    | 'wind_surge'
    | 'ev_surge'
    | 'industrial_surge'
    | 'asset_outage'
  label: string
  labelAr: string
  magnitude: number
  unit: string
  offsetMin: number
  durationMin: number
  regionCode?: string | null
  targetCode?: string | null
}

export interface ParsedScenario {
  events: ParsedEvent[]
  regionCode: string | null
  durationMin: number
  /** Fragments the parser recognised, for the preview. */
  understood: string[]
  /** Fragments it saw but could not place, so the operator can rephrase. */
  unresolved: string[]
  confidence: number
}

/** Region synonyms, including the compass-point forms people actually type. */
const REGION_TERMS: Array<{ code: string; terms: RegExp }> = [
  { code: 'RYD', terms: /(riyadh|الرياض)/i },
  { code: 'JED', terms: /(jeddah|jedda|جدة)/i },
  { code: 'DMM', terms: /(dammam|eastern|الدمام|الشرقية)/i },
  { code: 'MAK', terms: /(makkah|mecca|مكة)/i },
  { code: 'MED', terms: /(madinah|medina|المدينة)/i },
  { code: 'TUU', terms: /(tabuk|تبوك)/i },
  { code: 'NEO', terms: /(neom|نيوم)/i },
  { code: 'JAZ', terms: /(jazan|jizan|جازان)/i },
  { code: 'ABT', terms: /(abha|asir|أبها|عسير)/i },
  { code: 'QSM', terms: /(qassim|buraidah|القصيم|بريدة)/i },
]

interface SubjectRule {
  kind: ParsedEvent['kind']
  /** Words that name the thing being changed. */
  subject: RegExp
  /** Default unit and the sign convention the engine expects. */
  unit: string
  /** True when "decrease" is what the engine models as a positive magnitude. */
  invert: boolean
  label: string
  labelAr: string
}

const SUBJECTS: SubjectRule[] = [
  {
    kind: 'solar_drop',
    subject: /(solar|pv|الشمسية|الطاقة\s*الشمسية)/i,
    unit: '%',
    invert: true,
    label: 'Solar output',
    labelAr: 'الإنتاج الشمسي',
  },
  {
    kind: 'wind_surge',
    subject: /(wind|الرياح)/i,
    unit: '%',
    invert: true,
    label: 'Wind output',
    labelAr: 'إنتاج الرياح',
  },
  {
    kind: 'heat_wave',
    subject: /(temperature|heat|ambient|الحرارة|درجة\s*الحرارة|حر)/i,
    unit: '°C',
    invert: false,
    label: 'Ambient temperature',
    labelAr: 'الحرارة المحيطة',
  },
  {
    kind: 'ev_surge',
    subject: /(ev|electric\s*vehicle|charging|المركبات\s*الكهربائية|الشحن)/i,
    unit: '%',
    invert: false,
    label: 'EV charging demand',
    labelAr: 'طلب شحن المركبات',
  },
  {
    kind: 'industrial_surge',
    subject: /(industrial|factory|الصناعي|المصانع)/i,
    unit: '%',
    invert: false,
    label: 'Industrial demand',
    labelAr: 'الطلب الصناعي',
  },
  {
    kind: 'load_spike',
    subject: /(load|demand|consumption|الحمل|الطلب|الاستهلاك)/i,
    unit: '%',
    invert: false,
    label: 'Demand',
    labelAr: 'الطلب',
  },
]

const RISE = /(rise|rising|increase|increasing|surge|up|spike|ارتفاع|زيادة|رفع|ترتفع|يرتفع|قفزة)/i
const FALL = /(drop|fall|falling|decrease|decline|down|loss|انخفاض|هبوط|نقص|تنخفض|ينخفض|فقدان)/i

/** Arabic-Indic digits are normalised so a figure typed either way parses the same. */
function normaliseDigits(text: string): string {
  return text.replace(/[٠-٩]/g, (digit) =>
    String(digit.charCodeAt(0) - 0x0660),
  )
}

/** Minutes, from "for two hours", "لمدة ساعتين", "90 minutes". */
function parseDuration(text: string): number {
  if (/(ساعتين|two\s*hours|2\s*hours)/i.test(text)) return 120
  const hours = text.match(/(\d{1,2})\s*(?:hours?|h\b|ساعات?|ساعة)/i)
  if (hours) return clamp(Number(hours[1]) * 60, 15, 1440)
  const minutes = text.match(/(\d{1,3})\s*(?:minutes?|min\b|دقيقة|دقائق)/i)
  if (minutes) return clamp(Number(minutes[1]), 15, 1440)
  return 120
}

/**
 * Split into clauses around conjunctions.
 *
 * Each clause carries at most one subject and one quantity, which is how people write
 * these sentences: "load up 25 % **and** solar down 20 %".
 */
function clauses(text: string): string[] {
  return text
    .split(/\s+(?:and|with|plus|while|و|مع|بينما)\s+|[،,;]/i)
    .map((part) => part.trim())
    .filter((part) => part.length > 0)
}

/**
 * Parse a sentence into a runnable scenario.
 *
 * Everything is bounded on the way out — a request for a 900 % load rise is a typo or an
 * attack, not a question worth answering — and anything unmatched is reported rather
 * than guessed at.
 */
export function parseScenarioText(input: string): ParsedScenario {
  const text = normaliseDigits(input)
  const durationMin = parseDuration(text)

  const region = REGION_TERMS.find((entry) => entry.terms.test(text))
  const regionCode = region?.code ?? null

  const events: ParsedEvent[] = []
  const understood: string[] = []
  const unresolved: string[] = []

  for (const clause of clauses(text)) {
    const quantity = clause.match(/(-?\d{1,3}(?:\.\d+)?)\s*(%|٪|percent|بالمئة|درجات?|degrees?|°c|c\b|mw)?/i)
    const subject = SUBJECTS.find((rule) => rule.subject.test(clause))

    if (!subject || !quantity) {
      // A clause naming a subject with no number, or a number with no subject, is worth
      // reporting: it is usually a typo rather than noise.
      if (subject || quantity) unresolved.push(clause)
      continue
    }

    const raw = Math.abs(Number(quantity[1]))
    if (!Number.isFinite(raw) || raw === 0) {
      unresolved.push(clause)
      continue
    }

    const falling = FALL.test(clause)
    const rising = RISE.test(clause)
    // Direction comes from the verb where there is one; where there is not, the subject's
    // own convention decides — "solar 20 %" in a stress sentence means a shortfall.
    const isDecrease = falling || (!rising && subject.invert)

    const magnitude =
      subject.kind === 'heat_wave'
        ? round(clamp(raw, 0.5, 15), 1)
        : round(clamp(raw, 1, 80), 1)

    // The engine's own sign convention: solar and wind events carry the size of the
    // shortfall, demand events the size of the rise.
    if (subject.invert && !isDecrease) {
      unresolved.push(clause)
      continue
    }

    events.push({
      kind: subject.kind,
      label: `${subject.label} ${isDecrease && !subject.invert ? '−' : subject.invert ? '−' : '+'}${magnitude} ${subject.unit}`,
      labelAr: `${subject.labelAr} ${isDecrease && !subject.invert ? '−' : subject.invert ? '−' : '+'}${magnitude} ${subject.unit}`,
      magnitude,
      unit: subject.unit,
      offsetMin: 0,
      durationMin,
      regionCode,
    })
    understood.push(clause)
  }

  const assetMatch = text.match(/\b([A-Z]{1,3}-\d{2,4})\b/i)
  if (assetMatch && /(outage|fail|trip|خروج|تعطل|فصل)/i.test(text)) {
    events.push({
      kind: 'asset_outage',
      label: `${assetMatch[1].toUpperCase()} out of service`,
      labelAr: `${assetMatch[1].toUpperCase()} خارج الخدمة`,
      magnitude: 1,
      unit: '',
      offsetMin: 0,
      durationMin,
      regionCode,
      targetCode: assetMatch[1].toUpperCase(),
    })
    understood.push(assetMatch[0])
  }

  // Confidence is a function of how much of the sentence was actually placed, which is
  // the only thing the parser genuinely knows about its own performance.
  const parts = understood.length + unresolved.length
  const confidence =
    events.length === 0
      ? 0
      : round(clamp((understood.length / Math.max(parts, 1)) * 100 - (regionCode ? 0 : 8), 20, 96), 1)

  return { events, regionCode, durationMin, understood, unresolved, confidence }
}
