// 파일 ✨/mnt/d/D/_NR_/__AIWK_COMM_St/aiwk/AIWK_COMMON/core/aiwk_url_context.js
// 버전: v124-domain-uri-folder-rule
(function(){
  "use strict";
  const AIWK = window.AIWK || (window.AIWK = {});
  const BASE = "AIWK_COMMON/domains";

  function cleanPart(v){
    v = String(v == null ? "" : v).trim();
    try { v = decodeURIComponent(v); } catch(e) {}
    v = v.replace(/[\\]/g, "/").replace(/^\/+|\/+$/g, "");
    v = v.replace(/[^A-Za-z0-9._\-가-힣]/g, "_");
    return v;
  }

  function splitCleanPath(path){
    return String(path || "").split("/").filter(Boolean).map(cleanPart).filter(Boolean);
  }

  function splitHash(hash){
    hash = String(hash || "");
    if (!hash) return [];
    hash = hash.replace(/^#/, "");
    // Node-RED #flow/xxx처럼 브라우저 URL route 의미가 있는 hash는 후보 뒤쪽에 포함한다.
    return splitCleanPath(hash);
  }

  function isDefaultPort(protocol, port){
    protocol = String(protocol || "").toLowerCase();
    port = String(port || "");
    return !port || (protocol === "http:" && port === "80") || (protocol === "https:" && port === "443");
  }

  function md5(input){
    function cmn(q,a,b,x,s,t){ a = add32(add32(a,q), add32(x,t)); return add32((a << s) | (a >>> (32 - s)), b); }
    function ff(a,b,c,d,x,s,t){ return cmn((b & c) | ((~b) & d), a,b,x,s,t); }
    function gg(a,b,c,d,x,s,t){ return cmn((b & d) | (c & (~d)), a,b,x,s,t); }
    function hh(a,b,c,d,x,s,t){ return cmn(b ^ c ^ d, a,b,x,s,t); }
    function ii(a,b,c,d,x,s,t){ return cmn(c ^ (b | (~d)), a,b,x,s,t); }
    function md5cycle(x,k){
      let a=x[0], b=x[1], c=x[2], d=x[3];
      a=ff(a,b,c,d,k[0],7,-680876936); d=ff(d,a,b,c,k[1],12,-389564586); c=ff(c,d,a,b,k[2],17,606105819); b=ff(b,c,d,a,k[3],22,-1044525330);
      a=ff(a,b,c,d,k[4],7,-176418897); d=ff(d,a,b,c,k[5],12,1200080426); c=ff(c,d,a,b,k[6],17,-1473231341); b=ff(b,c,d,a,k[7],22,-45705983);
      a=ff(a,b,c,d,k[8],7,1770035416); d=ff(d,a,b,c,k[9],12,-1958414417); c=ff(c,d,a,b,k[10],17,-42063); b=ff(b,c,d,a,k[11],22,-1990404162);
      a=ff(a,b,c,d,k[12],7,1804603682); d=ff(d,a,b,c,k[13],12,-40341101); c=ff(c,d,a,b,k[14],17,-1502002290); b=ff(b,c,d,a,k[15],22,1236535329);
      a=gg(a,b,c,d,k[1],5,-165796510); d=gg(d,a,b,c,k[6],9,-1069501632); c=gg(c,d,a,b,k[11],14,643717713); b=gg(b,c,d,a,k[0],20,-373897302);
      a=gg(a,b,c,d,k[5],5,-701558691); d=gg(d,a,b,c,k[10],9,38016083); c=gg(c,d,a,b,k[15],14,-660478335); b=gg(b,c,d,a,k[4],20,-405537848);
      a=gg(a,b,c,d,k[9],5,568446438); d=gg(d,a,b,c,k[14],9,-1019803690); c=gg(c,d,a,b,k[3],14,-187363961); b=gg(b,c,d,a,k[8],20,1163531501);
      a=gg(a,b,c,d,k[13],5,-1444681467); d=gg(d,a,b,c,k[2],9,-51403784); c=gg(c,d,a,b,k[7],14,1735328473); b=gg(b,c,d,a,k[12],20,-1926607734);
      a=hh(a,b,c,d,k[5],4,-378558); d=hh(d,a,b,c,k[8],11,-2022574463); c=hh(c,d,a,b,k[11],16,1839030562); b=hh(b,c,d,a,k[14],23,-35309556);
      a=hh(a,b,c,d,k[1],4,-1530992060); d=hh(d,a,b,c,k[4],11,1272893353); c=hh(c,d,a,b,k[7],16,-155497632); b=hh(b,c,d,a,k[10],23,-1094730640);
      a=hh(a,b,c,d,k[13],4,681279174); d=hh(d,a,b,c,k[0],11,-358537222); c=hh(c,d,a,b,k[3],16,-722521979); b=hh(b,c,d,a,k[6],23,76029189);
      a=hh(a,b,c,d,k[9],4,-640364487); d=hh(d,a,b,c,k[12],11,-421815835); c=hh(c,d,a,b,k[15],16,530742520); b=hh(b,c,d,a,k[2],23,-995338651);
      a=ii(a,b,c,d,k[0],6,-198630844); d=ii(d,a,b,c,k[7],10,1126891415); c=ii(c,d,a,b,k[14],15,-1416354905); b=ii(b,c,d,a,k[5],21,-57434055);
      a=ii(a,b,c,d,k[12],6,1700485571); d=ii(d,a,b,c,k[3],10,-1894986606); c=ii(c,d,a,b,k[10],15,-1051523); b=ii(b,c,d,a,k[1],21,-2054922799);
      a=ii(a,b,c,d,k[8],6,1873313359); d=ii(d,a,b,c,k[15],10,-30611744); c=ii(c,d,a,b,k[6],15,-1560198380); b=ii(b,c,d,a,k[13],21,1309151649);
      a=ii(a,b,c,d,k[4],6,-145523070); d=ii(d,a,b,c,k[11],10,-1120210379); c=ii(c,d,a,b,k[2],15,718787259); b=ii(b,c,d,a,k[9],21,-343485551);
      x[0]=add32(a,x[0]); x[1]=add32(b,x[1]); x[2]=add32(c,x[2]); x[3]=add32(d,x[3]);
    }
    function md5blk(s){ const out=[]; for(let i=0;i<64;i+=4) out[i>>2]=s.charCodeAt(i)+(s.charCodeAt(i+1)<<8)+(s.charCodeAt(i+2)<<16)+(s.charCodeAt(i+3)<<24); return out; }
    function md51(s){
      s = unescape(encodeURIComponent(String(s == null ? "" : s)));
      const n=s.length; const state=[1732584193,-271733879,-1732584194,271733878]; let i;
      for(i=64;i<=n;i+=64) md5cycle(state, md5blk(s.substring(i-64,i)));
      s=s.substring(i-64); const tail=new Array(16).fill(0);
      for(i=0;i<s.length;i++) tail[i>>2] |= s.charCodeAt(i) << ((i%4)<<3);
      tail[i>>2] |= 0x80 << ((i%4)<<3);
      if(i>55){ md5cycle(state, tail); for(i=0;i<16;i++) tail[i]=0; }
      tail[14]=n*8; md5cycle(state, tail); return state;
    }
    function rhex(n){ let s="", j; for(j=0;j<4;j++) s += ("0" + ((n >> (j*8+4)) & 15).toString(16)).slice(-1) + ("0" + ((n >> (j*8)) & 15).toString(16)).slice(-1); return s; }
    function hex(x){ return x.map(rhex).join(""); }
    function add32(a,b){ return (a + b) & 0xFFFFFFFF; }
    return hex(md51(input));
  }

  function hostFolder(hostname, port, protocol){
    const host = cleanPart(String(hostname || "").toLowerCase());
    if (!host) return "";
    if (!isDefaultPort(protocol, port)) return host + "_" + cleanPart(port);
    return host;
  }

  function parse(url){
    let u;
    try { u = new URL(url || location.href); }
    catch(e){ return {ok:0, error:String(e && e.message || e), href:String(url||""), host:"", host_folder:"", port:"", path_parts:[], hash_parts:[], query:{}, hash:""}; }
    const parts = splitCleanPath(u.pathname);
    const hashParts = splitHash(u.hash);
    const query = {};
    try { u.searchParams.forEach((v,k)=>{ query[k]=v; }); } catch(e) {}
    const host = u.hostname.toLowerCase();
    const port = u.port || "";
    return {
      ok:1,
      href:u.href,
      protocol:u.protocol,
      host:host,
      host_folder:hostFolder(host, port, u.protocol),
      port:port,
      pathname:u.pathname,
      path_parts:parts,
      query:query,
      search:u.search || "",
      hash:u.hash || "",
      hash_parts:hashParts
    };
  }

  function candidateFolders(url){
    const ctx = typeof url === "object" && url && (url.host || url.ok) ? url : parse(url);
    const out = [];
    if(!ctx || !ctx.host_folder) return out;
    // v124 FIX: domains/ 루트는 공통 로드 후보가 아니다. 오직 domains/{HTTP_HOST[_PORT]}/{path...} URI 폴더만 후보다.
    let cur = BASE + "/" + cleanPart(ctx.host_folder);
    out.push(cur);
    (ctx.path_parts || []).forEach(function(p){ cur += "/" + p; out.push(cur); });
    (ctx.hash_parts || []).forEach(function(p){ cur += "/" + p; out.push(cur); });
    return out;
  }

  function lookupFolders(url){
    return candidateFolders(url).slice().reverse();
  }

  function makeContext(url){
    const ctx = parse(url);
    ctx.candidate_folders = candidateFolders(ctx);       // 상속 적용 순서: domains/{HTTP_HOST} → 최종 URL 폴더
    ctx.lookup_folders = lookupFolders(ctx);             // 검색 우선순위: 최종 URL 폴더 → domains/{HTTP_HOST}
    ctx.domain_route_key = [ctx.host_folder].concat(ctx.path_parts || [], ctx.hash_parts || []).filter(Boolean).join("/");
    ctx.url_md5 = md5(ctx.href || "");
    ctx.url_key = ctx.url_md5.slice(0, 5);
    ctx.route_key = ctx.url_key;
    ctx.wss_route_key = ctx.url_key;
    ctx.folder_route = (ctx.path_parts || []).join("/");
    ctx.hash_route = (ctx.hash_parts || []).join("/");
    ctx.version = "v127-route-key-urlmd5";
    return ctx;
  }

  AIWK.urlContext = {
    version:"v127-route-key-urlmd5",
    base:BASE,
    parse:parse,
    cleanPart:cleanPart,
    hostFolder:hostFolder,
    candidateFolders:candidateFolders,
    lookupFolders:lookupFolders,
    makeContext:makeContext,
    md5:md5
  };
}());
