const VALID_PIN_TYPES = new Set([
  "power_in",
  "power_out",
  "input",
  "output",
  "bidirectional",
  "passive",
  "analog",
  "ground"
]);

const VALID_NET_CLASSES = new Set([
  "power",
  "ground",
  "signal",
  "analog",
  "differential",
  "clock",
  "bus"
]);
const BUILTIN_PART_TYPES = new Set(["resistor", "capacitor", "inductor", "diode", "led", "connector", "generic"]);

const GENERIC_DEFAULT_WIDTH = 160;
const GENERIC_MIN_HEIGHT = 120;
const GENERIC_PIN_STEP = 18;
const TEMPLATE_PIN_STEP = 24;
const MIN_SYMBOL_WIDTH = 96;
const MIN_SYMBOL_HEIGHT = 64;

const TEMPLATE_DEFS = {
  resistor: {
    category: "passive_resistor",
    body: { width: 120, height: 70 },
    pins: [
      { name: "1", side: "left", offset: 35, type: "passive" },
      { name: "2", side: "right", offset: 35, type: "passive" }
    ]
  },
  capacitor: {
    category: "passive_capacitor",
    body: { width: 120, height: 70 },
    pins: [
      { name: "1", side: "left", offset: 35, type: "passive" },
      { name: "2", side: "right", offset: 35, type: "passive" }
    ]
  },
  inductor: {
    category: "passive_inductor",
    body: { width: 120, height: 70 },
    pins: [
      { name: "1", side: "left", offset: 35, type: "passive" },
      { name: "2", side: "right", offset: 35, type: "passive" }
    ]
  },
  diode: {
    category: "passive_diode",
    body: { width: 120, height: 70 },
    pins: [
      { name: "A", side: "left", offset: 35, type: "passive" },
      { name: "K", side: "right", offset: 35, type: "passive" }
    ]
  },
  led: {
    category: "passive_led",
    body: { width: 120, height: 70 },
    pins: [
      { name: "A", side: "left", offset: 35, type: "passive" },
      { name: "K", side: "right", offset: 35, type: "passive" }
    ]
  },
  connector: {
    category: "connector_generic",
    body: { width: 140, height: 90 },
    pins: [
      { name: "1", side: "left", offset: 24, type: "passive" },
      { name: "2", side: "left", offset: 48, type: "passive" },
      { name: "3", side: "right", offset: 24, type: "passive" },
      { name: "4", side: "right", offset: 48, type: "passive" }
    ]
  }
};

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}

function hasTemplateName(sym) {
  const name = String(sym?.template_name ?? "").toLowerCase();
  return Object.prototype.hasOwnProperty.call(TEMPLATE_DEFS, name);
}

function normalizePartName(value) {
  return String(value ?? "")
    .trim()
    .toLowerCase();
}

function symbolIdForPart(partName) {
  return `__part_${partName}`;
}

function isGenericSymbol(sym) {
  if (!sym || typeof sym !== "object") {
    return false;
  }
  if (sym.auto_generated === true) {
    return true;
  }
  const category = String(sym.category ?? "").toLowerCase();
  return category.includes("generic");
}

function pinTypeFromNet(netClass) {
  if (netClass === "ground") {
    return "ground";
  }
  if (netClass === "power") {
    return "power_in";
  }
  if (netClass === "clock") {
    return "input";
  }
  if (netClass === "analog") {
    return "analog";
  }
  return "passive";
}

function inferSymbolTemplate(inst) {
  const ref = String(inst.ref ?? "").toUpperCase();
  const symbol = String(inst.symbol ?? "").toLowerCase();
  const value = String(inst.properties?.value ?? "").toLowerCase();
  const haystack = `${symbol} ${value}`;

  if (ref.startsWith("R") || /\bres(istor)?\b/.test(haystack)) {
    return "resistor";
  }
  if (ref.startsWith("C") || /\bcap(acitor)?\b/.test(haystack)) {
    return "capacitor";
  }
  if (ref.startsWith("L") || /\bind(uctor)?\b/.test(haystack)) {
    return "inductor";
  }
  if (ref.startsWith("D") || /\bdiod(e)?\b/.test(haystack)) {
    return "diode";
  }
  if (/\bled\b/.test(haystack)) {
    return "led";
  }
  if (ref.startsWith("J") || ref.startsWith("P") || /\b(conn(ector)?|header)\b/.test(haystack)) {
    return "connector";
  }
  return null;
}

function buildTemplateSymbol(symbolId, templateName) {
  const template = TEMPLATE_DEFS[templateName];
  if (!template) {
    return null;
  }

  const pins = template.pins.map((p, idx) => ({
    name: p.name,
    number: String(idx + 1),
    side: p.side,
    offset: p.offset,
    type: p.type
  }));

  return {
    symbol_id: symbolId,
    category: template.category,
    auto_generated: true,
    template_name: templateName,
    body: {
      width: template.body.width,
      height: template.body.height
    },
    pins,
    graphics: {
      primitives: [
        {
          type: "rect",
          x: 0,
          y: 0,
          w: template.body.width,
          h: template.body.height
        }
      ]
    }
  };
}

function buildUsageByRef(model) {
  const usage = new Map();
  for (const net of model.nets) {
    for (const node of net.nodes ?? []) {
      const item = usage.get(node.ref) ?? new Map();
      const entry = item.get(node.pin) ?? { pin: node.pin, classes: new Set() };
      entry.classes.add(net.class);
      item.set(node.pin, entry);
      usage.set(node.ref, item);
    }
  }
  return usage;
}

function buildUsageBySymbol(model) {
  const byRef = new Map((model.instances ?? []).map((inst) => [inst.ref, inst]));
  const usage = new Map();

  for (const net of model.nets ?? []) {
    for (const node of net.nodes ?? []) {
      const inst = byRef.get(node.ref);
      if (!inst) {
        continue;
      }
      const symbolId = inst.symbol;
      const symbolUsage = usage.get(symbolId) ?? new Map();
      const entry = symbolUsage.get(node.pin) ?? { pin: node.pin, classes: new Set() };
      entry.classes.add(net.class);
      symbolUsage.set(node.pin, entry);
      usage.set(symbolId, symbolUsage);
    }
  }

  return usage;
}

function pinCountToHeight(pinCount) {
  const rows = Math.max(4, pinCount + 1);
  return Math.max(GENERIC_MIN_HEIGHT, rows * GENERIC_PIN_STEP);
}

function toNumericOffset(value, fallback) {
  const n = Number(value);
  return Number.isFinite(n) ? n : fallback;
}

function normalizeSymbolGeometry(sym, id, issues) {
  if (!sym || typeof sym !== "object" || !Array.isArray(sym.pins) || sym.pins.length === 0) {
    return;
  }

  const sideOrder = ["left", "right", "top", "bottom"];
  const sideBuckets = new Map(sideOrder.map((side) => [side, []]));

  for (const pin of sym.pins) {
    const side = sideBuckets.has(pin.side) ? pin.side : "left";
    sideBuckets.get(side).push(pin);
  }

  const maxPinsOnAnySide = Math.max(1, ...[...sideBuckets.values()].map((pins) => pins.length));
  const basePinStep = hasTemplateName(sym) ? TEMPLATE_PIN_STEP : GENERIC_PIN_STEP;
  const pinDensityBoost = Math.max(0, maxPinsOnAnySide - 4) * 2;
  const pinStep = Math.min(34, basePinStep + pinDensityBoost);
  const margin = pinStep;

  let changedOffsets = false;
  for (const side of sideOrder) {
    const pins = sideBuckets.get(side);
    pins.sort((a, b) => {
      const ao = toNumericOffset(a.offset, margin);
      const bo = toNumericOffset(b.offset, margin);
      if (ao !== bo) return ao - bo;
      return String(a.name ?? "").localeCompare(String(b.name ?? ""));
    });

    let next = margin;
    for (const pin of pins) {
      const current = toNumericOffset(pin.offset, next);
      const normalized = Math.max(next, current);
      if (pin.offset !== normalized) {
        pin.offset = normalized;
        changedOffsets = true;
      }
      next = normalized + pinStep;
    }
  }

  const maxVerticalOffset = Math.max(
    margin,
    ...[...(sideBuckets.get("left") ?? []), ...(sideBuckets.get("right") ?? [])].map((pin) =>
      toNumericOffset(pin.offset, margin)
    )
  );
  const maxHorizontalOffset = Math.max(
    margin,
    ...[...(sideBuckets.get("top") ?? []), ...(sideBuckets.get("bottom") ?? [])].map((pin) =>
      toNumericOffset(pin.offset, margin)
    )
  );

  const verticalPins = Math.max((sideBuckets.get("left") ?? []).length, (sideBuckets.get("right") ?? []).length);
  const horizontalPins = Math.max((sideBuckets.get("top") ?? []).length, (sideBuckets.get("bottom") ?? []).length);
  const labelGutterX = Math.min(88, 24 + maxPinsOnAnySide * 4);
  const labelGutterY = Math.min(52, 18 + Math.max(verticalPins, horizontalPins) * 2);
  const requiredHeight = Math.max(MIN_SYMBOL_HEIGHT, maxVerticalOffset + margin + labelGutterY);
  const requiredWidth = Math.max(MIN_SYMBOL_WIDTH, maxHorizontalOffset + margin + labelGutterX);

  sym.body = sym.body ?? {};
  const beforeWidth = toNumericOffset(sym.body.width, MIN_SYMBOL_WIDTH);
  const beforeHeight = toNumericOffset(sym.body.height, MIN_SYMBOL_HEIGHT);
  const nextWidth = Math.max(beforeWidth, requiredWidth);
  const nextHeight = Math.max(beforeHeight, requiredHeight);
  sym.body.width = nextWidth;
  sym.body.height = nextHeight;

  const desiredPinOrder = sideOrder.flatMap((side) => sideBuckets.get(side) ?? []);
  const changedOrder =
    desiredPinOrder.length === sym.pins.length &&
    desiredPinOrder.some((pin, index) => pin !== sym.pins[index]);
  if (changedOrder) {
    sym.pins = desiredPinOrder;
  }

  if (changedOffsets || nextWidth !== beforeWidth || nextHeight !== beforeHeight || changedOrder) {
    issues.push({
      code: "symbol_geometry_adjusted",
      message: `Adjusted symbol '${id}' geometry/pin spacing for readability.`,
      severity: "warning",
      path: `symbols.${id}`
    });
  }
}

function buildPinsFromUsage(ref, pinUsage) {
  const names = [...pinUsage.values()].map((x) => x.pin).sort((a, b) => a.localeCompare(b));
  if (!names.length) {
    return [
      {
        name: "P1",
        number: "1",
        side: "left",
        offset: GENERIC_PIN_STEP,
        type: "passive"
      }
    ];
  }

  const left = names.filter((_, idx) => idx % 2 === 0);
  const right = names.filter((_, idx) => idx % 2 === 1);
  let leftOffset = GENERIC_PIN_STEP;
  let rightOffset = GENERIC_PIN_STEP;

  const ordered = [...left, ...right];
  const pins = [];
  for (let i = 0; i < ordered.length; i += 1) {
    const name = ordered[i];
    const classes = [...(pinUsage.get(name)?.classes ?? new Set())].sort();
    const preferredClass = classes[0] ?? "signal";
    const side = left.includes(name) ? "left" : "right";
    const offset = side === "left" ? leftOffset : rightOffset;
    if (side === "left") {
      leftOffset += GENERIC_PIN_STEP;
    } else {
      rightOffset += GENERIC_PIN_STEP;
    }

    pins.push({
      name,
      number: String(i + 1),
      side,
      offset,
      type: pinTypeFromNet(preferredClass)
    });
  }

  return pins;
}

function ensureGenericSymbols(model, issues, enableGenericSymbols) {
  if (!enableGenericSymbols) {
    return model;
  }

  const next = clone(model);
  next.symbols = next.symbols ?? {};
  const usageByRef = buildUsageByRef(next);

  for (const inst of next.instances ?? []) {
    const part = normalizePartName(inst.part);
    if (!part) {
      continue;
    }

    if (!BUILTIN_PART_TYPES.has(part)) {
      issues.push({
        code: "invalid_part_type",
        message: `Instance '${inst.ref}' uses unsupported part '${inst.part}'.`,
        severity: "error",
        path: `instances.${inst.ref}.part`
      });
      continue;
    }

    inst.part = part;
    if (!inst.symbol) {
      inst.symbol = symbolIdForPart(part);
    }

    if (next.symbols[inst.symbol]) {
      continue;
    }

    if (part === "generic") {
      next.symbols[inst.symbol] = {
        symbol_id: inst.symbol,
        category: "generic",
        auto_generated: true
      };
      continue;
    }

    const templateSymbol = buildTemplateSymbol(inst.symbol, part);
    if (templateSymbol) {
      next.symbols[inst.symbol] = templateSymbol;
    }
  }

  for (const inst of next.instances ?? []) {
    if (next.symbols[inst.symbol]) {
      continue;
    }

    const templateName = inferSymbolTemplate(inst);
    const templated = templateName ? buildTemplateSymbol(inst.symbol, templateName) : null;

    if (templated) {
      next.symbols[inst.symbol] = templated;
      issues.push({
        code: "auto_template_symbol_created",
        message: `Created '${templateName}' symbol '${inst.symbol}' for instance '${inst.ref}'.`,
        severity: "warning",
        path: `instances.${inst.ref}.symbol`
      });
      continue;
    }

    const pinUsage = usageByRef.get(inst.ref) ?? new Map();
    const pins = buildPinsFromUsage(inst.ref, pinUsage);

    next.symbols[inst.symbol] = {
      symbol_id: inst.symbol,
      category: "generic",
      auto_generated: true,
      body: {
        width: GENERIC_DEFAULT_WIDTH,
        height: pinCountToHeight(pins.length)
      },
      pins,
      graphics: {
        primitives: [
          {
            type: "rect",
            x: 0,
            y: 0,
            w: GENERIC_DEFAULT_WIDTH,
            h: pinCountToHeight(pins.length)
          }
        ]
      }
    };

    issues.push({
      code: "auto_generic_symbol_created",
      message: `Created generic symbol '${inst.symbol}' for instance '${inst.ref}'.`,
      severity: "warning",
      path: `instances.${inst.ref}.symbol`
    });
  }

  const usageBySymbol = buildUsageBySymbol(next);
  for (const [id, sym] of Object.entries(next.symbols)) {
    if (typeof sym !== "object" || !sym) {
      continue;
    }

    if (!sym.symbol_id) {
      sym.symbol_id = id;
      issues.push({
        code: "auto_symbol_id_filled",
        message: `Filled missing symbol_id for '${id}'.`,
        severity: "warning",
        path: `symbols.${id}.symbol_id`
      });
    }

    const templateName = String(sym.template_name ?? "").toLowerCase();
    if (hasTemplateName(sym)) {
      const templated = buildTemplateSymbol(id, templateName);
      let templateHydrated = false;
      if (!sym.category) {
        sym.category = templated.category;
        issues.push({
          code: "auto_symbol_category_filled",
          message: `Filled missing category for '${id}' from template '${templateName}'.`,
          severity: "warning",
          path: `symbols.${id}.category`
        });
      }
      if (!sym.body || sym.body.width == null || sym.body.height == null) {
        sym.body = { ...(sym.body ?? {}), ...templated.body };
        templateHydrated = true;
      }
      if (!Array.isArray(sym.pins) || sym.pins.length === 0) {
        sym.pins = templated.pins;
        templateHydrated = true;
      }
      if (!sym.graphics) {
        sym.graphics = templated.graphics;
      }
      if (templateHydrated) {
        issues.push({
          code: "auto_template_symbol_hydrated",
          message: `Hydrated template fields for '${id}' (${templateName}).`,
          severity: "warning",
          path: `symbols.${id}`
        });
      }
      continue;
    }

    if (!sym.category) {
      sym.category = "generic";
      issues.push({
        code: "auto_symbol_category_filled",
        message: `Filled missing category for '${id}' as generic.`,
        severity: "warning",
        path: `symbols.${id}.category`
      });
    }

    const genericCategory = String(sym.category ?? "").toLowerCase().includes("generic");
    if (!genericCategory) {
      continue;
    }

    let genericHydrated = false;
    if (!Array.isArray(sym.pins) || sym.pins.length === 0) {
      const pinUsage = usageBySymbol.get(id) ?? new Map();
      sym.pins = buildPinsFromUsage(id, pinUsage);
      genericHydrated = true;
    }

    if (!sym.body || sym.body.width == null || sym.body.height == null) {
      sym.body = {
        width: sym.body?.width ?? GENERIC_DEFAULT_WIDTH,
        height: Math.max(sym.body?.height ?? GENERIC_MIN_HEIGHT, pinCountToHeight(sym.pins.length))
      };
      genericHydrated = true;
    }

    if (genericHydrated) {
      issues.push({
        code: "auto_generic_symbol_hydrated",
        message: `Hydrated generic fields for '${id}' from net usage.`,
        severity: "warning",
        path: `symbols.${id}`
      });
    }
  }

  for (const [id, sym] of Object.entries(next.symbols)) {
    if (isGenericSymbol(sym) || hasTemplateName(sym)) {
      normalizeSymbolGeometry(sym, id, issues);
    }
  }

  for (const net of next.nets ?? []) {
    for (const node of net.nodes ?? []) {
      const inst = (next.instances ?? []).find((x) => x.ref === node.ref);
      if (!inst) {
        continue;
      }
      const sym = next.symbols[inst.symbol];
      if (!sym || !isGenericSymbol(sym)) {
        continue;
      }

      const hasPin = Array.isArray(sym.pins) && sym.pins.some((p) => p.name === node.pin);
      if (hasPin) {
        continue;
      }

      const side = (sym.pins?.length ?? 0) % 2 === 0 ? "left" : "right";
      const sameSideCount = (sym.pins ?? []).filter((p) => p.side === side).length;
      const pinStep = sym.template_name ? TEMPLATE_PIN_STEP : GENERIC_PIN_STEP;
      const offset = pinStep + sameSideCount * pinStep;
      const nextNumber = String((sym.pins?.length ?? 0) + 1);

      sym.pins = sym.pins ?? [];
      sym.pins.push({
        name: node.pin,
        number: nextNumber,
        side,
        offset,
        type: pinTypeFromNet(net.class)
      });

      sym.body = sym.body ?? { width: GENERIC_DEFAULT_WIDTH, height: GENERIC_MIN_HEIGHT };
      sym.body.width = sym.body.width ?? GENERIC_DEFAULT_WIDTH;
      sym.body.height = Math.max(sym.body.height ?? GENERIC_MIN_HEIGHT, pinCountToHeight(sym.pins.length));

      issues.push({
        code: "auto_generic_pin_created",
        message: `Added pin '${node.pin}' to generic symbol '${inst.symbol}' from net '${net.name}'.`,
        severity: "warning",
        path: `symbols.${inst.symbol}.pins.${node.pin}`
      });
    }
  }

  return next;
}

export function validateModel(model, options = {}) {
  const issues = [];
  const enableGenericSymbols = options.generic_symbols !== false;

  if (!model || typeof model !== "object") {
    return {
      model: null,
      issues: [
        {
          code: "invalid_root",
          message: "Root payload must be an object.",
          severity: "error",
          path: "$"
        }
      ]
    };
  }

  if (!model.symbols || typeof model.symbols !== "object") {
    issues.push({
      code: "symbols_missing",
      message: "symbols must be an object map.",
      severity: "error",
      path: "symbols"
    });
  }
  if (!Array.isArray(model.instances)) {
    issues.push({
      code: "instances_missing",
      message: "instances must be an array.",
      severity: "error",
      path: "instances"
    });
  }
  if (!Array.isArray(model.nets)) {
    issues.push({
      code: "nets_missing",
      message: "nets must be an array.",
      severity: "error",
      path: "nets"
    });
  }

  if (issues.some((x) => x.severity === "error")) {
    return { model: null, issues };
  }

  const workingModel = ensureGenericSymbols(model, issues, enableGenericSymbols);
  const symbolIds = new Set(Object.keys(workingModel.symbols));

  for (const [id, sym] of Object.entries(workingModel.symbols)) {
    if (sym.symbol_id !== id) {
      issues.push({
        code: "symbol_id_mismatch",
        message: `symbol_id '${sym.symbol_id}' must match map key '${id}'.`,
        severity: "error",
        path: `symbols.${id}.symbol_id`
      });
    }

    if (!Array.isArray(sym.pins) || sym.pins.length === 0) {
      issues.push({
        code: "symbol_no_pins",
        message: `Symbol '${id}' has no pins.`,
        severity: "error",
        path: `symbols.${id}.pins`
      });
      continue;
    }

    const pinNames = new Set();
    for (const pin of sym.pins) {
      if (pinNames.has(pin.name)) {
        issues.push({
          code: "duplicate_pin_name",
          message: `Symbol '${id}' has duplicate pin '${pin.name}'.`,
          severity: "error",
          path: `symbols.${id}.pins`
        });
      }
      pinNames.add(pin.name);

      if (!VALID_PIN_TYPES.has(pin.type)) {
        issues.push({
          code: "invalid_pin_type",
          message: `Invalid pin type '${pin.type}' on ${id}.${pin.name}.`,
          severity: "error",
          path: `symbols.${id}.pins.${pin.name}.type`
        });
      }
    }
  }

  const refs = new Set();
  const instanceSymbol = new Map();
  for (const inst of workingModel.instances) {
    if (refs.has(inst.ref)) {
      issues.push({
        code: "duplicate_ref",
        message: `Duplicate instance ref '${inst.ref}'.`,
        severity: "error",
        path: "instances"
      });
    }
    refs.add(inst.ref);

    if (!inst.symbol) {
      issues.push({
        code: "instance_symbol_or_part_missing",
        message: `Instance '${inst.ref}' must define either 'symbol' or 'part'.`,
        severity: "error",
        path: `instances.${inst.ref}`
      });
    } else if (!symbolIds.has(inst.symbol)) {
      issues.push({
        code: "unknown_symbol",
        message: `Instance '${inst.ref}' references unknown symbol '${inst.symbol}'.`,
        severity: "error",
        path: `instances.${inst.ref}.symbol`
      });
    }
    instanceSymbol.set(inst.ref, inst.symbol);
  }

  const netNames = new Set();
  for (const net of workingModel.nets) {
    if (netNames.has(net.name)) {
      issues.push({
        code: "duplicate_net_name",
        message: `Duplicate net '${net.name}'.`,
        severity: "error",
        path: `nets.${net.name}`
      });
    }
    netNames.add(net.name);

    if (!VALID_NET_CLASSES.has(net.class)) {
      issues.push({
        code: "invalid_net_class",
        message: `Invalid net class '${net.class}' on '${net.name}'.`,
        severity: "error",
        path: `nets.${net.name}.class`
      });
    }

    if (!Array.isArray(net.nodes) || net.nodes.length < 2) {
      issues.push({
        code: "net_too_small",
        message: `Net '${net.name}' must contain at least two nodes.`,
        severity: "error",
        path: `nets.${net.name}.nodes`
      });
      continue;
    }

    for (const node of net.nodes) {
      const symId = instanceSymbol.get(node.ref);
      if (!symId) {
        issues.push({
          code: "unknown_ref_in_net",
          message: `Net '${net.name}' references unknown component '${node.ref}'.`,
          severity: "error",
          path: `nets.${net.name}.nodes`
        });
        continue;
      }
      const sym = workingModel.symbols[symId];
      if (!sym || !Array.isArray(sym.pins)) {
        continue;
      }
      const foundPin = sym.pins.some((p) => p.name === node.pin);
      if (!foundPin) {
        issues.push({
          code: "unknown_pin_in_net",
          message: `Net '${net.name}' references unknown pin '${node.ref}.${node.pin}'.`,
          severity: "error",
          path: `nets.${net.name}.nodes`
        });
      }
    }
  }

  return {
    model: issues.some((x) => x.severity === "error") ? null : workingModel,
    issues
  };
}
