import { ReflectUtils } from '@dooboostore/core/reflect/ReflectUtils';
import { getInnerHtmlMetadataList } from './innerHtml';
import { getQueryMetadata } from './query';
import { getQueryAllMetadata } from './queryAll';
import { getAddEventListenerMetadata } from './addEventListener';
import { getLifecycleMetadata, ON_BEFORE_CONNECTED_METADATA_KEY, ON_AFTER_CONNECTED_METADATA_KEY, ON_BEFORE_DISCONNECTED_METADATA_KEY, ON_AFTER_DISCONNECTED_METADATA_KEY, ON_BEFORE_ADOPTED_METADATA_KEY, ON_AFTER_ADOPTED_METADATA_KEY, ON_ADD_EVENT_LISTENER_METADATA_KEY, ON_ATTRIBUTE_CHANGED_METADATA_KEY, ATTRIBUTE_CHANGED_WILDCARD, getMutationSubscribeMetadataList } from './lifecycles';
import { getAttributeMetadataList } from './attribute';
import { getStateMetadataList } from './state';
import { getEmitCustomEventMetadataList } from './emitCustomEvent';
import { SwcUtils, SWC_NOT_FOUND } from '../utils/Utils';
import { HTML_TAG_ENTRIES } from '../config/config';

export interface ElementConfig {
  name: string;
  extends?: string;
  observedAttributes?: string[];
  customElementRegistry?: CustomElementRegistry;
  autoRemoveEventListeners?: boolean;
  window?: Window;
}

export const ELEMENT_CONFIG_KEY = Symbol('simple-web-component:element-config');
export const STATE_CHANGE_EVENT = 'swc:state-change';

// --- 환경 초기화 헬퍼 ---

function buildEnv(configWindow?: Window) {
  const win: any = configWindow || (typeof window !== 'undefined' ? window : undefined);
  const doc: Document = win?.document;

  const builtInTagMap = new Map<any, string>();
  for (const [cls, tag] of HTML_TAG_ENTRIES) {
    if (win?.[cls]) builtInTagMap.set(win[cls], tag);
  }

  const domHelpers = {
    $d: doc,
    $w: win,
    $q: (sel: string, root?: Element | Document | ShadowRoot) => (root ?? doc).querySelector(sel),
    $qa: (sel: string, root?: Element | Document | ShadowRoot) => Array.from((root ?? doc).querySelectorAll(sel)),
    $qi: (id: string, root?: Document | ShadowRoot) => (root ?? doc).getElementById(id)
  } as const;

  return { win, doc, builtInTagMap, domHelpers };
}

export const elementDefine =
  (inConfig: ElementConfig | string): ClassDecorator =>
  (constructor: any) => {
    const config: ElementConfig = typeof inConfig === 'string' ? { name: inConfig } : inConfig;
    const { win, doc, builtInTagMap: BUILT_IN_TAG_MAP, domHelpers: SWC_DOM_HELPERS } = buildEnv(config.window);
    config.window = win;
    const SWC_DOM_HELPER_KEYS = Object.keys(SWC_DOM_HELPERS) as (keyof typeof SWC_DOM_HELPERS)[];
    const SWC_DOM_HELPER_VALS = () => SWC_DOM_HELPER_KEYS.map(k => (SWC_DOM_HELPERS as any)[k]);

    let extendsTagName = config.extends;
    if (!extendsTagName) {
      let proto = constructor;
      while (proto && proto !== HTMLElement && proto !== Function.prototype) {
        extendsTagName = BUILT_IN_TAG_MAP.get(proto);
        if (extendsTagName) break;
        proto = Object.getPrototypeOf(proto);
      }
      config.extends = extendsTagName;
    }

    const attributePropsList = getAttributeMetadataList(constructor);
    const emitCustomEventList = getEmitCustomEventMetadataList(constructor);
    const stateList = getStateMetadataList(constructor);
    const observedFromProps = attributePropsList ? attributePropsList.map(it => it.options.name!) : [];
    const observedFromEmits = emitCustomEventList ? emitCustomEventList.map(it => it.options.attributeName!) : [];
    const swcLifecycleAttributes = ['swc-on-constructor', 'swc-on-before-connected', 'swc-on-after-connected', 'swc-on-before-disconnected', 'swc-on-after-disconnected', 'swc-on-before-adopted', 'swc-on-after-adopted', 'swc-on-attribute-changed'];
    const mergedObservedAttributes = [...new Set([...(config.observedAttributes ?? []), ...observedFromProps, ...observedFromEmits, ...swcLifecycleAttributes])];

    const NewClass = class extends (constructor as any) {
      private _swcId = Math.random().toString(36).substring(2, 11);
      private _observer: MutationObserver | null = null;
      private _shadowObserver: MutationObserver | null = null;
      private _boundElements = new WeakMap<Element, Set<string | symbol>>();
      private _activeListeners: Array<{ el: Element | HTMLElement; type: string; handler: EventListener; options?: any }> = [];
      private _emitHandlers = new Map<string, EventListener>();
      private _stateBindings = new Map<string, any[]>();
      private _internalStates = new Map<string | symbol, any>();
      private _externalSources = new Map<string, HTMLElement>();

      // name→value 컨텍스트 반환 (applyData의 hostContext용)
      _getHostContext(): Record<string, any> {
        const ctx: Record<string, any> = {};
        if (stateList) {
          stateList.forEach(meta => {
            const name = meta.options.name!;
            const val = this._internalStates.get(meta.propertyKey);
            ctx[name] = val;
          });
        }
        if (attributePropsList) {
          attributePropsList.forEach(meta => {
            const name = meta.options.name!;
            const val = this._internalStates.get(meta.propertyKey);
            ctx[name] = val;
          });
        }
        return ctx;
      }

      static get observedAttributes() {
        return mergedObservedAttributes;
      }

      private _executeSwcScript(attrName: string, extraArgs: Record<string, any> = {}) {
        const script = this.getAttribute(attrName);
        if (script) {
          try {
            const $hosts = SwcUtils.findAllSwcHosts(this as any);
            const $firstHost = $hosts[0] || null;
            const $lastHost = $hosts[$hosts.length - 1] || null;
            const $host = $lastHost;
            const args = { $host, $hosts, $firstHost, $lastHost, ...SWC_DOM_HELPERS, ...extraArgs };
            const argNames = Object.keys(args);
            const argValues = Object.values(args);
            new Function(...argNames, script).apply(this, argValues);
          } catch (e) {
            console.error(`[SWC] Failed to execute ${attrName} script:`, e);
          }
        }
      }

      constructor(...args: any[]) {
        super(...args);
        this._executeSwcScript('swc-on-constructor');

        if (stateList) {
          stateList.forEach(meta => {
            const key = meta.propertyKey;
            const stateName = meta.options.name!;
            const initialVal = (this as any)[key];
            this._internalStates.set(
              key,
              SwcUtils.createReactiveProxy(initialVal, () => this._updateState(stateName))
            );
            Object.defineProperty(this, key, {
              get: () => this._internalStates.get(key),
              set: newVal => {
                if (this._internalStates.get(key) === newVal) return;
                this._internalStates.set(
                  key,
                  SwcUtils.createReactiveProxy(newVal, () => this._updateState(stateName))
                );
                this._updateState(stateName);
              },
              enumerable: true,
              configurable: true
            });
          });
        }

        if (attributePropsList) {
          attributePropsList.forEach(meta => {
            const key = meta.propertyKey;
            const attrName = meta.options.name!;
            const attrValue = this.getAttribute(attrName);
            let initialVal = attrValue !== null ? attrValue : (this as any)[key];
            if (initialVal === undefined) initialVal = null;

            if (attrValue !== null) {
              if (meta.options.type === Number) initialVal = Number(attrValue);
              else if (meta.options.type === Boolean) initialVal = true;
              else if (meta.options.type === Object) {
                try {
                  initialVal = JSON.parse(attrValue);
                } catch (e) {}
              }
            }

            this._internalStates.set(key, initialVal);
            Object.defineProperty(this, key, {
              get: () => this._internalStates.get(key),
              set: newVal => {
                if (this._internalStates.get(key) === newVal) return;
                this._internalStates.set(key, newVal);
                if (typeof (this as any)._updateState === 'function') (this as any)._updateState(attrName);
                // 객체/배열은 setAttribute로 반영하지 않음 - attributeChangedCallback 무한루프 방지
                if (!meta.options.disableReflect && this.setAttribute && typeof newVal !== 'object') {
                  if (newVal === null || newVal === undefined || newVal === false) this.removeAttribute(attrName);
                  else this.setAttribute(attrName, String(newVal));
                }
              },
              enumerable: true,
              configurable: true
            });
          });
        }

        const innerHtmlList = getInnerHtmlMetadataList(this);
        if (innerHtmlList?.some(it => it.options.useShadow === true) && !this.shadowRoot) {
          this.attachShadow({ mode: 'open' });
        }
      }

      private _isSyncing = false;

      private _syncDecorators() {
        if (this._isSyncing) return;
        this._isSyncing = true;
        try {
          this._buildStateMap();
          this._syncEventListenersAndQueries();
        } finally {
          this._isSyncing = false;
        }
      }

      private _syncEventListenersAndQueries() {
        const getSearchRoots = (rootOption?: string): Node[] => {
          const roots: Node[] = [];
          if (rootOption === 'shadow') {
            if (this.shadowRoot) roots.push(this.shadowRoot);
          } else if (rootOption === 'light') {
            roots.push(this as any as Node);
          } else if (rootOption === 'all') {
            if (this.shadowRoot) roots.push(this.shadowRoot);
            roots.push(this as any as Node);
          } else {
            roots.push(this.shadowRoot || (this as any as Node));
          }
          return roots;
        };

        const queryMetadata = getQueryMetadata(this);
        if (queryMetadata) {
          queryMetadata
            .filter(it => it.isMethod)
            .forEach(it => {
              const searchRoots = getSearchRoots(it.options.root);
              let foundEl: Element | null = null;
              for (const root of searchRoots) {
                foundEl = it.selector ? (root as HTMLElement).querySelector(it.selector) : (this as any as Element);
                if (foundEl) break;
              }
              if (foundEl) {
                let bound = this._boundElements.get(foundEl);
                if (!bound) {
                  bound = new Set();
                  this._boundElements.set(foundEl, bound);
                }
                if (!bound.has(it.propertyKey)) {
                  (this as any)[it.propertyKey](foundEl);
                  bound.add(it.propertyKey);
                }
              }
            });
        }

        const queryAllMetadata = getQueryAllMetadata(this);
        if (queryAllMetadata) {
          queryAllMetadata
            .filter(it => it.isMethod)
            .forEach(it => {
              const searchRoots = getSearchRoots(it.options.root);
              const allElements: Element[] = [];
              searchRoots.forEach(root => {
                const found = it.selector ? (root as HTMLElement).querySelectorAll(it.selector) : [this as any as Element];
                allElements.push(...Array.from(found));
              });
              (this as any)[it.propertyKey](allElements);
            });
        }

        const eventListeners = getAddEventListenerMetadata(this);
        if (eventListeners) {
          eventListeners.forEach(it => {
            const { query, type, root: rootOption, ...options } = it.options;
            const searchRoots = getSearchRoots(rootOption);
            searchRoots.forEach(root => {
              const targetElements = query ? (root as HTMLElement).querySelectorAll(query) : [this as any as Element];
              targetElements.forEach(targetElement => {
                if (targetElement) {
                  let bound = this._boundElements.get(targetElement);
                  if (!bound) {
                    bound = new Set();
                    this._boundElements.set(targetElement, bound);
                  }
                  const eventKey = `event:${String(it.propertyKey)}:${type}`;
                  if (!bound.has(eventKey)) {
                    const handler = (event: any) => {
                      if (it.options.stopImmediatePropagation) event.stopImmediatePropagation();
                      if (it.options.stopPropagation) event.stopPropagation();
                      if (it.options.preventDefault) event.preventDefault();
                      (this as any)[it.propertyKey](event, targetElement);
                    };
                    targetElement.addEventListener(type, handler, options);
                    bound.add(eventKey);
                    if (config.autoRemoveEventListeners) this._activeListeners.push({ el: targetElement, type, handler, options });
                    const addEventMethods = getLifecycleMetadata(this, ON_ADD_EVENT_LISTENER_METADATA_KEY);
                    addEventMethods?.forEach((m: string | symbol) => {
                      if (typeof (this as any)[m] === 'function') (this as any)[m](targetElement, type, handler);
                    });
                  }
                }
              });
            });
          });
        }
      }

      private _buildStateMap() {
        const scan = (root: Node) => {
          const walker = doc.createTreeWalker(root, (win as any).NodeFilter.SHOW_ELEMENT | (win as any).NodeFilter.SHOW_TEXT);
          let node: Node | null = null;
          while ((node = walker.nextNode())) {
            if (node.nodeType === Node.TEXT_NODE) this._parseAndBind(node, 'text');
            else if (node.nodeType === Node.ELEMENT_NODE) {
              const el = node as HTMLElement;

              const alias = el.getAttribute('as');
              if (alias && !this._externalSources.has(alias)) {
                this._externalSources.set(alias, el);
                const aliasKey = `alias-event:${alias}`;
                let bound = this._boundElements.get(el);
                if (!bound) {
                  bound = new Set();
                  this._boundElements.set(el, bound);
                }
                if (!bound.has(aliasKey)) {
                  el.addEventListener(STATE_CHANGE_EVENT, () => this._updateState(alias));
                  bound.add(aliasKey);
                }
              }

              Array.from(el.attributes).forEach(attr => {
                if (attr.name.startsWith('swc-on-') && !swcLifecycleAttributes.includes(attr.name)) {
                  const eventName = attr.name.substring(7);
                  if (eventName === 'connected') {
                    const eventKey = `attr-once:${attr.name}`;
                    let bound = this._boundElements.get(el);
                    if (!bound) {
                      bound = new Set();
                      this._boundElements.set(el, bound);
                    }
                    if (!bound.has(eventKey)) {
                      const script = el.getAttribute(attr.name);
                      if (script) {
                        try {
                          const $hosts = SwcUtils.findAllSwcHosts(el);
                          const $firstHost = $hosts[0] || null;
                          const $lastHost = $hosts[$hosts.length - 1] || null;
                          const $host = $lastHost;
                          new Function('event', '$data', '$host', '$hosts', '$firstHost', '$lastHost', ...SWC_DOM_HELPER_KEYS, script).call(el, null, null, $host, $hosts, $firstHost, $lastHost, ...SWC_DOM_HELPER_VALS());
                        } catch (e) {
                          console.error(`[SWC] Failed to execute ${attr.name} script:`, e);
                        }
                      }
                      bound.add(eventKey);
                    }
                    return;
                  }

                  const eventKey = `attr-event:${attr.name}`;
                  let bound = this._boundElements.get(el);
                  if (!bound) {
                    bound = new Set();
                    this._boundElements.set(el, bound);
                  }
                  if (!bound.has(eventKey)) {
                    const handler = (event: any) => {
                      const rawScript = el.getAttribute(attr.name);
                      if (rawScript) {
                        try {
                          const $hosts = SwcUtils.findAllSwcHosts(el);
                          const $firstHost = $hosts[0] || null;
                          const $lastHost = $hosts[$hosts.length - 1] || null;
                          const $host = $lastHost;
                          new Function('event', '$data', '$host', '$hosts', '$firstHost', '$lastHost', ...SWC_DOM_HELPER_KEYS, rawScript).call(el, event, (event as CustomEvent).detail, $host, $hosts, $firstHost, $lastHost, ...SWC_DOM_HELPER_VALS());
                        } catch (e) {
                          console.error(`[SWC] Failed to execute ${attr.name} script:`, e);
                        }
                      }
                    };
                    el.addEventListener(eventName, handler);

                    bound.add(eventKey);
                    if (config.autoRemoveEventListeners) this._activeListeners.push({ el, type: eventName, handler });
                  }
                }
                this._parseAndBind(attr, 'attribute', el);
              });

              // --- Scope Protection: Don't scan inside child SWC components ---
              // is 어트리뷰트 방식 커스텀 엘리먼트도 포함 (swc-for-of-*, swc-if-* 등)
              const isSwcChild = (el as any)._swcId || getElementConfig(el.constructor) || (el.getAttribute('is')?.startsWith('swc-') ?? false);
              if (isSwcChild) {
                const next = walker.nextSibling();
                if (!next) break;
                node = next;
                if (node.nodeType === Node.TEXT_NODE) this._parseAndBind(node, 'text');
              }
            }
          }
        };
        if (this.shadowRoot) scan(this.shadowRoot);
        scan(this as any as Node);
      }

      private _parseAndBind(node: Node | Attr, type: 'text' | 'attribute', owner?: HTMLElement) {
        const tplKey = `__swc_original_${this._swcId}`;
        const isAlreadyBound = (node as any).__swc_bound_ids?.has(this._swcId);
        const content = (node as any)[tplKey] || node.textContent || '';
        const matches = Array.from(content.matchAll(/{{(.*?)}}/g)) as any[];
        if (matches.length === 0 || isAlreadyBound) return;

        matches.forEach(match => {
          const fullPath = (match[1] as string).trim();
          const rootName = fullPath.split('.')[0];
          const isState = stateList?.some(s => s.options.name === rootName);
          const isAttr = attributePropsList?.some(a => a.options.name === rootName);
          const isLogicKey = (this as any)._asKey === rootName || (this as any)._asIndexKey === rootName;
          const isExternal = this._externalSources.has(rootName);
          const isSelfAlias = this.getAttribute('as') === rootName;
          const isInstanceProp = rootName in this;
          if (!isState && !isAttr && !isLogicKey && !isExternal && !isSelfAlias && !isInstanceProp) return;

          if (!this._stateBindings.has(rootName)) this._stateBindings.set(rootName, []);
          if (!(node as any)[tplKey]) (node as any)[tplKey] = content;
          if (!(node as any).__swc_bound_ids) (node as any).__swc_bound_ids = new Set();
          (node as any).__swc_bound_ids.add(this._swcId);
          this._stateBindings.get(rootName)!.push({ node, type, owner, path: fullPath });
          this._updateState(rootName);
        });
      }

      private _isUpdatingState = false;

      private _updateState(stateName: string) {
        if (!this._stateBindings) return;
        if (this._isUpdatingState) return;
        this._isUpdatingState = true;
        try {
          this._executeBindingUpdate(stateName);
          const selfAlias = this.getAttribute('as');
          if (selfAlias && selfAlias !== stateName) this._executeBindingUpdate(selfAlias);
          this.dispatchEvent(new CustomEvent(STATE_CHANGE_EVENT, { bubbles: true, composed: true }));
        } finally {
          this._isUpdatingState = false;
        }
      }

      private _executeBindingUpdate(stateName: string) {
        const bindings = this._stateBindings.get(stateName);
        if (!bindings) return;
        const tplKey = `__swc_original_${this._swcId}`;
        bindings.forEach(bin => {
          let text = (bin.node as any)[tplKey];
          if (!text) return;
          const matches = Array.from(text.matchAll(/{{(.*?)}}/g)) as RegExpMatchArray[];
          let updatedText = text;
          for (const match of matches) {
            const path = match[1].trim();
            const root = path.split('.')[0];
            let val: any = undefined;
            let current: HTMLElement | null = this as any as HTMLElement;
            while (current) {
              const currentNewClass = current as any;
              if (current.getAttribute('as') === root) {
                val = SwcUtils.getValueByPath(current, path.split('.').slice(1).join('.') || 'value', 'value');
                if (val !== SWC_NOT_FOUND) break;
              }
              const externalSource = currentNewClass._externalSources?.get(root);
              if (externalSource) {
                val = SwcUtils.getValueByPath(externalSource, path.split('.').slice(1).join('.') || 'value', 'value');
                if (val !== SWC_NOT_FOUND) break;
              }
              const cStates = getStateMetadataList(current.constructor);
              const cAttrs = getAttributeMetadataList(current.constructor);
              const sMeta = cStates?.find(s => s.options.name === root);
              const aMeta = cAttrs?.find(a => a.options.name === root);
              if (sMeta || aMeta) {
                // 프로퍼티 키로 인스턴스에서 루트 값을 먼저 가져온 뒤 나머지 경로를 탐색
                const propKey = sMeta ? String(sMeta.propertyKey) : String(aMeta!.propertyKey);
                const rootVal = (current as any)[propKey];
                const subPath = path.split('.').slice(1).join('.');
                if (subPath) {
                  // superUser.name → rootVal = superUser 객체, subPath = "name"
                  val = SwcUtils.getValueByPath(rootVal, subPath, subPath.split('.')[0]);
                  if (val === SWC_NOT_FOUND) {
                    // 단순 점 경로 직접 접근
                    const parts = subPath.split('.');
                    let cur = rootVal;
                    for (const p of parts) {
                      if (cur === null || cur === undefined) {
                        cur = SWC_NOT_FOUND;
                        break;
                      }
                      cur = cur[p];
                    }
                    if (cur !== undefined && cur !== SWC_NOT_FOUND) val = cur;
                  }
                } else {
                  // 루트 자체가 값 (e.g. {{superUser}} 전체)
                  val = rootVal;
                }
                if (val !== SWC_NOT_FOUND && val !== undefined) break;
              }

              // Fallback: check raw property on instance even if not @state
              if (root in current) {
                val = SwcUtils.getValueByPath(current, path, root);
                if (val !== SWC_NOT_FOUND) break;
              }

              current = current.parentElement || (current.getRootNode() as any).host;
            }
            if (val !== SWC_NOT_FOUND && val !== undefined) {
              const strVal = val === null ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
              updatedText = updatedText.replace(match[0], strVal);
              if (bin.type === 'attribute' && bin.owner) {
                const attrName = (bin.node as Attr).name;
                const ownerAttrs = getAttributeMetadataList(bin.owner.constructor);
                const attrMeta = ownerAttrs?.find(a => a.options.name === attrName);
                const propName = attrMeta ? String(attrMeta.propertyKey) : attrName.replace(/-([a-z])/g, g => g[1].toUpperCase());

                if (val === null || val === undefined) {
                  bin.owner.removeAttribute(attrName);
                  if (propName in bin.owner) (bin.owner as any)[propName] = val;
                } else {
                  if (propName in bin.owner) (bin.owner as any)[propName] = val;
                  bin.owner.setAttribute(attrName, updatedText);
                  if ((attrName === 'value' || attrName === 'checked') && bin.owner.tagName.match(/INPUT|TEXTAREA|SELECT/)) {
                    (bin.owner as any)[attrName] = updatedText;
                  }
                }
              }
            }
          }
          if (bin.type === 'text') bin.node.textContent = updatedText;
        });
      }

      disconnectedCallback() {
        this._executeSwcScript('swc-on-before-disconnected');
        const bMethods = getLifecycleMetadata<(string | symbol)[]>(this, ON_BEFORE_DISCONNECTED_METADATA_KEY);
        bMethods?.forEach(m => {
          if (typeof (this as any)[m] === 'function') (this as any)[m]();
        });
        if (this._observer) this._observer.disconnect();
        if (this._shadowObserver) this._shadowObserver.disconnect();
        if (config.autoRemoveEventListeners) {
          this._activeListeners.forEach(({ el, type, handler, options }) => el.removeEventListener(type, handler, options));
          this._activeListeners = [];
        }
        if (super.disconnectedCallback) super.disconnectedCallback();
        const aMethods = getLifecycleMetadata<(string | symbol)[]>(this, ON_AFTER_DISCONNECTED_METADATA_KEY);
        aMethods?.forEach(m => {
          if (typeof (this as any)[m] === 'function') (this as any)[m]();
        });
        this._executeSwcScript('swc-on-after-disconnected');
      }

      adoptedCallback() {
        this._executeSwcScript('swc-on-before-adopted');
        const bMethods = getLifecycleMetadata<(string | symbol)[]>(this, ON_BEFORE_ADOPTED_METADATA_KEY);
        bMethods?.forEach(m => {
          if (typeof (this as any)[m] === 'function') (this as any)[m]();
        });
        if (super.adoptedCallback) super.adoptedCallback();
        const aMethods = getLifecycleMetadata<(string | symbol)[]>(this, ON_AFTER_ADOPTED_METADATA_KEY);
        aMethods?.forEach(m => {
          if (typeof (this as any)[m] === 'function') (this as any)[m]();
        });
        this._executeSwcScript('swc-on-after-adopted');
      }

      async connectedCallback() {
        this._executeSwcScript('swc-on-before-connected');
        const bMethods = getLifecycleMetadata<(string | symbol)[]>(this, ON_BEFORE_CONNECTED_METADATA_KEY);
        bMethods?.forEach(m => {
          if (typeof (this as any)[m] === 'function') (this as any)[m]();
        });
        if (typeof (this as any).initCore === 'function') (this as any).initCore();
        const iHtmlList = getInnerHtmlMetadataList(this);
        let sContent = '';
        let lContent = '';
        if (iHtmlList) {
          for (const meta of iHtmlList) {
            const result = await (this as any)[meta.propertyKey]();
            if (result !== undefined) {
              if (meta.options.useShadow === true) sContent += result;
              else lContent += result;
            }
          }
        }
        if (this.shadowRoot) this.shadowRoot.innerHTML = sContent;
        if (lContent) this.innerHTML = lContent;
        (this as any)._syncDecorators();

        const isManagedByParent = !!SwcUtils.findNearestSwcHost(this as any);

        if (!isManagedByParent) {
          Array.from(this.attributes as any).forEach((attr: any) => {
            if (attr.name.startsWith('swc-on-') && swcLifecycleAttributes.indexOf(attr.name) === -1) {
              const eventName = attr.name.substring(7);
              if (eventName === 'connected') {
                const eventKey = `host-attr-once:${attr.name}`;
                let bound = this._boundElements.get(this as any);
                if (!bound) {
                  bound = new Set();
                  this._boundElements.set(this as any, bound);
                }
                if (!bound.has(eventKey)) {
                  const script = (this as any).getAttribute(attr.name);
                  if (script) {
                    try {
                      const $hosts = SwcUtils.findAllSwcHosts(this as any);
                      const $firstHost = $hosts[0] || null;
                      const $lastHost = $hosts[$hosts.length - 1] || null;
                      const $host = $lastHost;
                      new Function('event', '$data', '$host', '$hosts', '$firstHost', '$lastHost', ...SWC_DOM_HELPER_KEYS, script).call(this, null, null, $host, $hosts, $firstHost, $lastHost, ...SWC_DOM_HELPER_VALS());
                    } catch (e) {
                      console.error(`[SWC] Failed to execute ${attr.name} script:`, e);
                    }
                  }
                  bound.add(eventKey);
                }
                return;
              }

              const eventKey = `host-attr-event:${attr.name}`;
              let bound = this._boundElements.get(this as any);
              if (!bound) {
                bound = new Set();
                this._boundElements.set(this as any, bound);
              }

              if (!bound.has(eventKey)) {
                const handler = (event: any) => {
                  const script = (this as any).getAttribute(attr.name);
                  if (script) {
                    try {
                      const $hosts = SwcUtils.findAllSwcHosts(this as any);
                      const $firstHost = $hosts[0] || null;
                      const $lastHost = $hosts[$hosts.length - 1] || null;
                      const $host = $lastHost;
                      new Function('event', '$data', '$host', '$hosts', '$firstHost', '$lastHost', ...SWC_DOM_HELPER_KEYS, script).call(this, event, (event as CustomEvent).detail, $host, $hosts, $firstHost, $lastHost, ...SWC_DOM_HELPER_VALS());
                    } catch (e) {
                      console.error(`[SWC] Failed to execute ${attr.name} script:`, e);
                    }
                  }
                };
                this.addEventListener(eventName, handler);

                bound.add(eventKey);
                if (config.autoRemoveEventListeners) this._activeListeners.push({ el: this as any, type: eventName, handler });
              }
            }
          });
        }

        const mutationMetaList = getMutationSubscribeMetadataList(this);

        const fireMutationCallbacks = (mutationList: MutationRecord[], observer: MutationObserver, root: 'light' | 'shadow') => {
          mutationMetaList?.forEach(meta => {
            const r = meta.options.root ?? 'auto';
            const matches = r === 'all' || r === root || (r === 'auto' && (this.shadowRoot ? root === 'shadow' : root === 'light'));
            if (matches && typeof (this as any)[meta.propertyKey] === 'function') {
              (this as any)[meta.propertyKey](mutationList, observer, root);
            }
          });
        };

        this._observer = new (win as any).MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
          (this as any)._syncDecorators();
          fireMutationCallbacks(mutationList, observer, 'light');
        });
        // @ts-ignore
        this._observer.observe(this as any, { childList: true, subtree: true });

        if (this.shadowRoot) {
          this._shadowObserver = new (win as any).MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
            (this as any)._syncDecorators();
            fireMutationCallbacks(mutationList, observer, 'shadow');
          });
          // @ts-ignore
          this._shadowObserver.observe(this.shadowRoot, { childList: true, subtree: true });
        }
        if (super.connectedCallback) await super.connectedCallback();
        const aMethods = getLifecycleMetadata<(string | symbol)[]>(this, ON_AFTER_CONNECTED_METADATA_KEY);
        aMethods?.forEach(m => {
          if (typeof (this as any)[m] === 'function') (this as any)[m]();
        });
        this._executeSwcScript('swc-on-after-connected');
      }

      attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
        if (super.attributeChangedCallback) super.attributeChangedCallback(name, oldValue, newValue);
        if (attributePropsList) {
          attributePropsList.forEach(meta => {
            if (meta.options.name === name) {
              const current = (this as any)[meta.propertyKey];
              // 객체/배열 타입은 attributeChangedCallback에서 setter를 재호출하지 않음
              // (setter에서 setAttribute를 하지 않으므로 이 경로로 들어올 일이 없고,
              //  외부에서 attribute를 직접 바꾸는 경우만 처리)
              if (typeof current === 'object' && current !== null) return;
              if (String(current) !== String(newValue)) (this as any)[meta.propertyKey] = newValue;
            }
          });
        }
        if (typeof (this as any)._updateState === 'function') (this as any)._updateState(name);
        const aMethodsMap = getLifecycleMetadata(constructor, ON_ATTRIBUTE_CHANGED_METADATA_KEY) as Map<string, (string | symbol)[]>;
        const mKeys = aMethodsMap?.get(name);
        if (mKeys && Array.isArray(mKeys))
          mKeys.forEach(key => {
            if (typeof (this as any)[key] === 'function') (this as any)[key](newValue, oldValue, name);
          });
        if (emitCustomEventList) {
          const eMeta = emitCustomEventList.find(it => it.options.attributeName === name);
          if (eMeta) {
            const eType = eMeta.options.type;
            const oHandler = this._emitHandlers.get(eType);
            if (oHandler) this.removeEventListener(eType, oHandler);
            if (newValue) {
              const nHandler = (e: Event) => {
                new Function('event', '$data', newValue).call(this, e, (e as CustomEvent).detail);
              };
              this.addEventListener(eType, nHandler);
              this._emitHandlers.set(eType, nHandler);
            }
          }
        }
        const wMethodsKeys = aMethodsMap?.get(ATTRIBUTE_CHANGED_WILDCARD);
        if (wMethodsKeys && Array.isArray(wMethodsKeys))
          wMethodsKeys.forEach(key => {
            if (typeof (this as any)[key] === 'function') (this as any)[key](newValue, oldValue, name);
          });
        this._executeSwcScript('swc-on-attribute-changed', { $new: newValue, $old: oldValue, $name: name });
      }
    };

    ReflectUtils.defineMetadata(ELEMENT_CONFIG_KEY, config, NewClass);
    ReflectUtils.defineMetadata(ELEMENT_CONFIG_KEY, config, constructor);

    const registry = config.customElementRegistry || (typeof win !== 'undefined' ? win.customElements : undefined);
    if (registry && !registry.get(config.name)) {
      const options = config.extends ? { extends: config.extends } : undefined;
      registry.define(config.name, NewClass as any, options);
    }

    return NewClass as any;
  };

export const getElementConfig = (target: any): ElementConfig | undefined => {
  const constructor = target instanceof Function ? target : target.constructor;
  return ReflectUtils.getMetadata(ELEMENT_CONFIG_KEY, constructor);
};
