(function (root, factory) {
  "use strict";

  const api = Object.freeze(factory());
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnIndustryWorkflow = api;
})(typeof globalThis === "object" ? globalThis : this, function () {
  "use strict";

  const DEFAULT_FADE_DURATION = 6;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const codePattern = /^[A-Za-z0-9]{1,32}$/;
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
  const datePattern = /^\d{4}-\d{2}-\d{2}$/;
  const markets = Object.freeze(["kospi", "kosdaq"]);
  const runtimeAssets = Object.freeze({
    kospi: Object.freeze({
      relativePath: "bin/Debug/Res/업종_코스피.ini",
      sha256: "F21DE58003315EAB41F9FE7B5A573C71653056203E6743D05AA96E7FF1B8BF52"
    }),
    kosdaq: Object.freeze({
      relativePath: "bin/Debug/Res/업종_코스닥.ini",
      sha256: "F25B8926E8F20FC5FBC94A1891A9E2EAD22852E8C829321EF1A001FE3F42ECE4"
    })
  });

  const periodCuts = [
    ["5일", "8061", "FiveDays"], ["20일", "8040", "TwentyDays"],
    ["60일", "8046", "SixtyDays"], ["120일", "8051", "OneHundredTwentyDays"],
    ["240일", "8056", "TwoHundredFortyDays"]
  ];
  const candleCuts = [["일봉", "8035"], ...periodCuts.map(([label, code]) => [label, code])];

  function cut(id, label, section, kind, details = {}) {
    return Object.freeze({ id, label, section, kind, ...details });
  }

  const cuts = Object.freeze([
    cut("one-column", "1열판", "업종 판", "single", { builderKey: "s5001" }),
    cut("two-column", "2열판", "업종 판", "pair", { builderKey: "s8018" }),
    ...periodCuts.map(([label, , period]) =>
      cut(`yield-${label}`, `수익률그래프_${label}`, "수익률 그래프", "yield", {
        builderKey: "s5086", period
      })),
    ...candleCuts.map(([label, code]) =>
      cut(`candle-${label}`, `캔들그래프_${label}`, "캔들 그래프", "candle", {
        builderKey: "s8010", code, mode: "PRICE"
      })),
    ...candleCuts.map(([label, code]) =>
      cut(`candle-volume-${label}`, `캔들그래프(거래량)_${label}`, "캔들 그래프 · 거래량", "candle", {
        builderKey: "s8010", code, mode: "VOLUME"
      })),
    cut("five-row", "5단 표그래프", "시장 전체", "fixed", { builderKey: "s5074" }),
    cut("square", "네모그래프", "시장 전체", "fixed", { builderKey: "s8001" }),
    cut("sector", "섹터지수", "시장 전체", "fixed", { builderKey: "s5078" })
  ]);
  if (cuts.length !== 22 || new Set(cuts.map(value => value.id)).size !== 22) {
    throw new Error("Each legacy industry tab must contain exactly 22 cut actions.");
  }
  const cutsById = new Map(cuts.map(value => [value.id, value]));

  function cleanText(value, maximumLength = 128) {
    if (typeof value !== "string") return null;
    const text = value.trim();
    return text && text.length <= maximumLength && !controlCharacterPattern.test(text) ? text : null;
  }

  function normalizeMarket(value) {
    const text = String(value || "").trim().toLocaleLowerCase("en-US");
    return markets.includes(text) ? text : null;
  }

  function normalizeIndustry(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return null;
    const market = normalizeMarket(value.market);
    const name = cleanText(value.name ?? value.industryName);
    const code = cleanText(value.code ?? value.industryCode, 32);
    if (!market || !name || !code || !codePattern.test(code)) return null;
    return Object.freeze({ market, name, code });
  }

  function localDateText(now = new Date()) {
    const year = String(now.getFullYear()).padStart(4, "0");
    const month = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  }

  function requireOptions(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Industry playlist options are required.");
    }
    const id = cleanText(options.id);
    if (!id || !identifierPattern.test(id)) throw new TypeError("A safe industry playlist id is required.");
    const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : Number(options.fadeDuration);
    if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) {
      throw new RangeError("Fade duration must be an integer from 0 through 60.");
    }
    const now = options.now instanceof Date && !Number.isNaN(options.now.valueOf()) ? options.now : new Date();
    const ma5 = options.ma5 === undefined ? false : options.ma5;
    const ma20 = options.ma20 === undefined ? false : options.ma20;
    if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") {
      throw new TypeError("Moving-average flags must be boolean values.");
    }
    return { id, fadeDuration, now, ma5, ma20 };
  }

  function mappingFor(market, selectedCut, selected, primary, secondary, now, movingAverages) {
    const industryGroup = market === "kospi" ? "INDUSTRY_KOSPI" : "INDUSTRY_KOSDAQ";
    const candleGroup = market === "kospi" ? "KOSPI_INDUSTRY" : "KOSDAQ_INDUSTRY";
    const pagedGroup = market === "kospi" ? "PAGED_DOMESTIC_KOSPI" : "PAGED_DOMESTIC_KOSDAQ";
    const marketName = market === "kospi" ? "KOSPI" : "KOSDAQ";
    switch (selectedCut.kind) {
      case "single":
        if (!selected) throw new RangeError("Select one industry first.");
        return {
          code: "5001",
          selection: {
            groupCode: industryGroup, subject: selected.name,
            graphicType: "1열판", subtype: "CURRENT", dataCode: ""
          }
        };
      case "pair":
        if (!primary || !secondary || primary.name.includes("-") || secondary.name.includes("-")) {
          throw new RangeError("Select two delimiter-safe industries first.");
        }
        return {
          code: market === "kospi" ? "8018" : "8032",
          selection: {
            groupCode: industryGroup, subject: `${primary.name}-${secondary.name}`,
            graphicType: "2열판", subtype: "", dataCode: ""
          }
        };
      case "yield":
        if (!selected) throw new RangeError("Select one industry first.");
        return {
          code: "5086",
          selection: {
            groupCode: industryGroup, subject: selected.name,
            graphicType: "YIELD_GRAPH", subtype: selectedCut.period, dataCode: ""
          }
        };
      case "candle":
        if (!selected) throw new RangeError("Select one industry first.");
        const flags = [];
        if (movingAverages.ma5) flags.push("MA5");
        if (movingAverages.ma20) flags.push("MA20");
        return {
          code: selectedCut.code,
          selection: {
            groupCode: candleGroup, subject: selected.name,
            graphicType: flags.join(","), subtype: selectedCut.mode, dataCode: ""
          }
        };
      case "fixed":
        if (selectedCut.id === "five-row") {
          return {
            code: "5074",
            selection: {
              groupCode: pagedGroup, subject: "INDEX",
              graphicType: "", subtype: "INDUSTRY", dataCode: localDateText(now)
            }
          };
        }
        if (selectedCut.id === "square") {
          return {
            code: market === "kospi" ? "8001" : "8002",
            selection: {
              groupCode: industryGroup, subject: marketName,
              graphicType: "네모그래프", subtype: "", dataCode: ""
            }
          };
        }
        return {
          code: "5078",
          selection: {
            groupCode: industryGroup, subject: marketName,
            graphicType: "SECTOR_INDEX", subtype: "", dataCode: ""
          }
        };
      default:
        throw new Error("The selected industry cut is unsupported.");
    }
  }

  function isCutAllowed(marketValue, cutId, selectedValue, primaryValue, secondaryValue) {
    const market = normalizeMarket(marketValue);
    const selectedCut = cutsById.get(cutId);
    const selected = normalizeIndustry(selectedValue);
    const primary = normalizeIndustry(primaryValue);
    const secondary = normalizeIndustry(secondaryValue);
    if (!market || !selectedCut) return false;
    if (selected && selected.market !== market) return false;
    if (primary && primary.market !== market) return false;
    if (secondary && secondary.market !== market) return false;
    if (selectedCut.kind === "fixed") return true;
    if (selectedCut.kind === "pair") {
      return Boolean(primary && secondary && !primary.name.includes("-") && !secondary.name.includes("-"));
    }
    return Boolean(selected);
  }

  function createIndustryPlaylistEntry(marketValue, cutId, options) {
    const market = normalizeMarket(marketValue);
    const selectedCut = cutsById.get(cutId);
    if (!market || !selectedCut) throw new RangeError("The industry action is unknown.");
    const selected = normalizeIndustry(options?.selected);
    const pair = Array.isArray(options?.pair) ? options.pair : [];
    const primary = normalizeIndustry(pair[0]);
    const secondary = normalizeIndustry(pair[1]);
    if ((selected && selected.market !== market) || (primary && primary.market !== market) ||
        (secondary && secondary.market !== market)) {
      throw new RangeError("The selected industry market is inconsistent.");
    }
    if (!isCutAllowed(market, cutId, selected, primary, secondary)) {
      throw new RangeError(selectedCut.kind === "pair" ? "Select two industries first." : "Select one industry first.");
    }
    const normalized = requireOptions(options);
    const movingAverages = Object.freeze({
      ma5: selectedCut.kind === "candle" && normalized.ma5,
      ma20: selectedCut.kind === "candle" && normalized.ma20
    });
    const mapping = mappingFor(
      market,
      selectedCut,
      selected,
      primary,
      secondary,
      normalized.now,
      movingAverages);
    const selection = Object.freeze(mapping.selection);
    return {
      id: normalized.id,
      builderKey: selectedCut.builderKey,
      code: mapping.code,
      aliases: [mapping.code],
      title: selectedCut.kind === "fixed"
        ? `${market === "kospi" ? "코스피" : "코스닥"} ${selectedCut.label}`
        : (selectedCut.kind === "pair" ? `${primary.name}-${secondary.name}` : selected.name),
      detail: selectedCut.label,
      category: selectedCut.kind === "candle" || selectedCut.kind === "yield" ? "chart" : "plate",
      market,
      selection,
      enabled: true,
      fadeDuration: normalized.fadeDuration,
      graphicType: selection.graphicType,
      subtype: selection.subtype,
      operator: Object.freeze({
        source: "legacy-industry-workflow",
        cutId: selectedCut.id,
        market,
        ...(selectedCut.kind === "pair" ? { pair: Object.freeze([primary, secondary]) } : {}),
        ...(selectedCut.kind !== "pair" && selectedCut.kind !== "fixed" ? { selected } : {}),
        assetPath: runtimeAssets[market].relativePath,
        assetSha256: runtimeAssets[market].sha256,
        movingAverages
      })
    };
  }

  function sameSelection(left, right, allowDynamicDate) {
    if (!left || typeof left !== "object" || !right || typeof right !== "object") return false;
    return ["groupCode", "subject", "graphicType", "subtype", "dataCode"].every(key => {
      if (allowDynamicDate && key === "dataCode") return typeof left[key] === "string" && datePattern.test(left[key]);
      return typeof left[key] === "string" && left[key] === right[key];
    });
  }

  function restoreIndustryPlaylistEntry(value, options = {}) {
    if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null;
    const operator = value.operator;
    if (!operator || typeof operator !== "object" || operator.source !== "legacy-industry-workflow") return null;
    const market = normalizeMarket(operator.market);
    const selectedCut = cutsById.get(operator.cutId);
    if (!market || !selectedCut || operator.assetPath !== runtimeAssets[market].relativePath ||
        operator.assetSha256 !== runtimeAssets[market].sha256) return null;
    let movingAverages = operator.movingAverages;
    if (movingAverages === undefined) {
      if (selectedCut.kind === "candle") {
        const graphicType = value?.selection?.graphicType;
        if (!["", "MA5", "MA20", "MA5,MA20"].includes(graphicType)) return null;
        movingAverages = {
          ma5: graphicType === "MA5" || graphicType === "MA5,MA20",
          ma20: graphicType === "MA20" || graphicType === "MA5,MA20"
        };
      } else {
        movingAverages = { ma5: false, ma20: false };
      }
    }
    if (!movingAverages || typeof movingAverages !== "object" || Array.isArray(movingAverages) ||
        typeof movingAverages.ma5 !== "boolean" || typeof movingAverages.ma20 !== "boolean" ||
        Object.keys(movingAverages).length !== 2 ||
        (selectedCut.kind !== "candle" && (movingAverages.ma5 || movingAverages.ma20))) return null;
    let recreated;
    try {
      recreated = createIndustryPlaylistEntry(market, selectedCut.id, {
        id: value.id,
        fadeDuration: value.fadeDuration,
        selected: operator.selected,
        pair: operator.pair,
        now: options.now,
        ma5: movingAverages.ma5,
        ma20: movingAverages.ma20
      });
    } catch {
      return null;
    }
    const scalars = ["id", "builderKey", "code", "title", "detail", "category", "market", "fadeDuration"];
    if (!scalars.every(key => value[key] === recreated[key]) ||
        !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== recreated.code ||
        !sameSelection(value.selection, recreated.selection, selectedCut.id === "five-row")) return null;
    return { ...recreated, enabled: value.enabled };
  }

  return {
    markets,
    runtimeAssets,
    cuts,
    normalizeIndustry,
    isCutAllowed,
    localDateText,
    createIndustryPlaylistEntry,
    restoreIndustryPlaylistEntry,
    refreshIndustryPlaylistEntry: restoreIndustryPlaylistEntry
  };
});
