// Core electrical anomaly detection engine.
// Runs entirely in-browser: smart header mapping, number parsing,
// unbalance-current and under-voltage detection.

import * as XLSX from "xlsx";

export type Phase = "R" | "S" | "T";

export interface DetectionThresholds {
  /** Unbalance % above this = warning. */
  unbalanceWarn: number;
  /** Unbalance % above this = critical. */
  unbalanceCritical: number;
  /** Voltage below referenceVoltage * (1 - warn/100) = warning. */
  underVoltageWarnPct: number;
  /** Voltage below referenceVoltage * (1 - critical/100) = critical. */
  underVoltageCriticalPct: number;
}

export const DEFAULT_THRESHOLDS: DetectionThresholds = {
  unbalanceWarn: 10,
  unbalanceCritical: 20,
  underVoltageWarnPct: 5,
  underVoltageCriticalPct: 10,
};

export type Severity = "ok" | "warning" | "critical";

export interface AnalyzedRow {
  rowNumber: number;
  timestamp: string | null;
  currents: Record<Phase, number | null>;
  voltages: Record<Phase, number | null>;
  currentAvg: number | null;
  unbalancePercentage: number | null;
  minVoltage: number | null;
  isUnbalance: boolean;
  isUnderVoltage: boolean;
  severity: Severity;
}

export interface FieldMapping {
  timestamp: string | null;
  currentR: string | null;
  currentS: string | null;
  currentT: string | null;
  voltageR: string | null;
  voltageS: string | null;
  voltageT: string | null;
}

export interface AnalysisResult {
  id: string;
  filename: string;
  uploadDate: string;
  totalRows: number;
  anomalyCount: number;
  unbalanceCount: number;
  underVoltageCount: number;
  criticalCount: number;
  warningCount: number;
  referenceVoltage: number;
  thresholds: DetectionThresholds;
  mapping: FieldMapping;
  detectedColumns: string[];
  rows: AnalyzedRow[];
}

/** Parse "59,50" / "1.234,56" / "1234.5" into a float. */
export function parseNumeric(value: unknown): number | null {
  if (value === null || value === undefined || value === "") return null;
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
  let s = String(value).trim();
  if (!s || s === "." || s === "-") return null;
  const hasComma = s.includes(",");
  const hasDot = s.includes(".");
  if (hasComma && hasDot) {
    // Assume "." thousands + "," decimal (EU) e.g. 1.234,56
    s = s.replace(/\./g, "").replace(",", ".");
  } else if (hasComma) {
    s = s.replace(",", ".");
  }
  s = s.replace(/[^0-9.\-]/g, "");
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : null;
}

function normalize(h: string): string {
  return h.toLowerCase().replace(/[\s._\-/\\()]+/g, " ").trim();
}

function tokenize(h: string): string[] {
  return normalize(h).split(" ").filter(Boolean);
}

// Multi-character metric words (safe as substrings).
const CURRENT_WORDS = ["current", "arus", "ampere", "amp"];
const VOLTAGE_WORDS = ["voltage", "tegangan", "volt"];
const TIME_WORDS = ["datetime", "timestamp", "waktu", "tanggal", "date", "time"];

// Exact single/short tokens that imply a metric+phase (e.g. "Ir", "Vt").
const CURRENT_PHASE_TOKENS: Record<Phase, string[]> = {
  R: ["ir", "ia"],
  S: ["is", "ib"],
  T: ["it", "ic"],
};
const VOLTAGE_PHASE_TOKENS: Record<Phase, string[]> = {
  R: ["vr", "va"],
  S: ["vs", "vb"],
  T: ["vt", "vc"],
};

// Exact tokens that select a phase, matched by word boundary (never substring).
const PHASE_TOKENS: Record<Phase, string[]> = {
  R: ["r", "a", "l1", "r1", "ir", "ia", "vr", "va"],
  S: ["s", "b", "l2", "s2", "is", "ib", "vs", "vb"],
  T: ["t", "c", "l3", "t3", "it", "ic", "vt", "vc"],
};

function hasWord(norm: string, words: string[]): boolean {
  return words.some((w) => norm.includes(w));
}

function matchesPhase(tokens: string[], phase: Phase): boolean {
  const set = new Set(tokens);
  // Explicit "ph a" / "phase c" pairs.
  const idx = tokens.findIndex((t) => t === "ph" || t === "phase" || t === "fase");
  const letterFor: Record<Phase, string[]> = { R: ["a", "r"], S: ["b", "s"], T: ["c", "t"] };
  if (idx >= 0 && idx + 1 < tokens.length && letterFor[phase].includes(tokens[idx + 1])) {
    return true;
  }
  return PHASE_TOKENS[phase].some((t) => set.has(t));
}

/** Fuzzy-map real headers to canonical electrical fields. */
export function detectMapping(headers: string[]): FieldMapping {
  const mapping: FieldMapping = {
    timestamp: null,
    currentR: null,
    currentS: null,
    currentT: null,
    voltageR: null,
    voltageS: null,
    voltageT: null,
  };

  for (const raw of headers) {
    const norm = normalize(raw);
    const tokens = tokenize(raw);

    if (!mapping.timestamp && hasWord(norm, TIME_WORDS)) {
      mapping.timestamp = raw;
      continue;
    }

    const isCurrent = hasWord(norm, CURRENT_WORDS);
    const isVoltage = hasWord(norm, VOLTAGE_WORDS);

    for (const phase of ["R", "S", "T"] as Phase[]) {
      const cKey = `current${phase}` as keyof FieldMapping;
      const vKey = `voltage${phase}` as keyof FieldMapping;

      // Standalone tokens like "Ir" / "Vt".
      if (!mapping[cKey] && tokens.some((t) => CURRENT_PHASE_TOKENS[phase].includes(t))) {
        mapping[cKey] = raw;
        continue;
      }
      if (!mapping[vKey] && tokens.some((t) => VOLTAGE_PHASE_TOKENS[phase].includes(t))) {
        mapping[vKey] = raw;
        continue;
      }

      if (!isCurrent && !isVoltage) continue;
      if (!matchesPhase(tokens, phase)) continue;

      if (isCurrent && !mapping[cKey]) mapping[cKey] = raw;
      else if (isVoltage && !mapping[vKey]) mapping[vKey] = raw;
    }
  }

  return mapping;
}

export function median(nums: number[]): number {
  if (nums.length === 0) return 0;
  const s = [...nums].sort((a, b) => a - b);
  const mid = Math.floor(s.length / 2);
  return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
}

export interface ParsedFile {
  headers: string[];
  records: Record<string, unknown>[];
}

export async function parseFile(file: File): Promise<ParsedFile> {
  const buf = await file.arrayBuffer();
  const wb = XLSX.read(buf, { type: "array" });
  // Pick the sheet with the most rows.
  let best = wb.SheetNames[0];
  let bestCount = -1;
  for (const name of wb.SheetNames) {
    const ref = wb.Sheets[name]["!ref"];
    if (!ref) continue;
    const range = XLSX.utils.decode_range(ref);
    const count = range.e.r - range.s.r;
    if (count > bestCount) {
      bestCount = count;
      best = name;
    }
  }
  const ws = wb.Sheets[best];
  const records = XLSX.utils.sheet_to_json<Record<string, unknown>>(ws, { defval: null });
  const headers = records.length ? Object.keys(records[0]) : [];
  return { headers, records };
}

export function analyze(
  parsed: ParsedFile,
  filename: string,
  thresholds: DetectionThresholds = DEFAULT_THRESHOLDS,
  mappingOverride?: FieldMapping,
): AnalysisResult {
  const mapping = mappingOverride ?? detectMapping(parsed.headers);

  // Reference voltage = median of all mapped voltage readings.
  const allVoltages: number[] = [];
  const vKeys = [mapping.voltageR, mapping.voltageS, mapping.voltageT].filter(Boolean) as string[];
  for (const rec of parsed.records) {
    for (const k of vKeys) {
      const v = parseNumeric(rec[k]);
      if (v !== null && v > 0) allVoltages.push(v);
    }
  }
  const referenceVoltage = median(allVoltages);
  const warnV = referenceVoltage * (1 - thresholds.underVoltageWarnPct / 100);
  const critV = referenceVoltage * (1 - thresholds.underVoltageCriticalPct / 100);

  const rows: AnalyzedRow[] = parsed.records.map((rec, idx) => {
    const currents: Record<Phase, number | null> = {
      R: mapping.currentR ? parseNumeric(rec[mapping.currentR]) : null,
      S: mapping.currentS ? parseNumeric(rec[mapping.currentS]) : null,
      T: mapping.currentT ? parseNumeric(rec[mapping.currentT]) : null,
    };
    const voltages: Record<Phase, number | null> = {
      R: mapping.voltageR ? parseNumeric(rec[mapping.voltageR]) : null,
      S: mapping.voltageS ? parseNumeric(rec[mapping.voltageS]) : null,
      T: mapping.voltageT ? parseNumeric(rec[mapping.voltageT]) : null,
    };

    const cVals = (Object.values(currents).filter((v) => v !== null) as number[]);
    const currentAvg = cVals.length ? cVals.reduce((a, b) => a + b, 0) / cVals.length : null;
    let unbalancePercentage: number | null = null;
    if (currentAvg !== null && currentAvg > 0 && cVals.length >= 2) {
      const maxDev = Math.max(...cVals.map((v) => Math.abs(v - currentAvg)));
      unbalancePercentage = (maxDev / currentAvg) * 100;
    }

    const vVals = (Object.values(voltages).filter((v) => v !== null) as number[]);
    const minVoltage = vVals.length ? Math.min(...vVals) : null;

    const isUnbalance =
      unbalancePercentage !== null && unbalancePercentage >= thresholds.unbalanceWarn;
    const isUnderVoltage =
      minVoltage !== null && referenceVoltage > 0 && minVoltage < warnV;

    let severity: Severity = "ok";
    const critical =
      (unbalancePercentage !== null && unbalancePercentage >= thresholds.unbalanceCritical) ||
      (minVoltage !== null && referenceVoltage > 0 && minVoltage < critV);
    if (critical) severity = "critical";
    else if (isUnbalance || isUnderVoltage) severity = "warning";

    return {
      rowNumber: idx + 1,
      timestamp: mapping.timestamp ? (rec[mapping.timestamp] as string | null) ?? null : null,
      currents,
      voltages,
      currentAvg,
      unbalancePercentage,
      minVoltage,
      isUnbalance,
      isUnderVoltage,
      severity,
    };
  });

  const unbalanceCount = rows.filter((r) => r.isUnbalance).length;
  const underVoltageCount = rows.filter((r) => r.isUnderVoltage).length;
  const criticalCount = rows.filter((r) => r.severity === "critical").length;
  const warningCount = rows.filter((r) => r.severity === "warning").length;
  const anomalyCount = rows.filter((r) => r.severity !== "ok").length;

  return {
    id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    filename,
    uploadDate: new Date().toISOString(),
    totalRows: rows.length,
    anomalyCount,
    unbalanceCount,
    underVoltageCount,
    criticalCount,
    warningCount,
    referenceVoltage,
    thresholds,
    mapping,
    detectedColumns: parsed.headers,
    rows,
  };
}

export function exportToCsv(result: AnalysisResult): string {
  const header = [
    "row_number",
    "timestamp",
    "current_R",
    "current_S",
    "current_T",
    "current_avg",
    "voltage_R",
    "voltage_S",
    "voltage_T",
    "min_voltage",
    "unbalance_percentage",
    "is_unbalance",
    "is_under_voltage",
    "severity",
  ];
  const lines = [header.join(",")];
  for (const r of result.rows) {
    const fmt = (n: number | null) => (n === null ? "" : n.toFixed(2));
    lines.push(
      [
        r.rowNumber,
        r.timestamp ? `"${String(r.timestamp).replace(/"/g, '""')}"` : "",
        fmt(r.currents.R),
        fmt(r.currents.S),
        fmt(r.currents.T),
        fmt(r.currentAvg),
        fmt(r.voltages.R),
        fmt(r.voltages.S),
        fmt(r.voltages.T),
        fmt(r.minVoltage),
        fmt(r.unbalancePercentage),
        r.isUnbalance,
        r.isUnderVoltage,
        r.severity,
      ].join(","),
    );
  }
  return lines.join("\n");
}
