package com.anythink.reactnative.nativead;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.view.View;

import com.thinkup.core.api.TUAdInfo;
import com.thinkup.core.api.TUAdSourceStatusListener;
import com.thinkup.core.api.TUNetworkConfirmInfo;
import com.thinkup.core.api.TUShowConfig;
import com.thinkup.core.api.AdError;
import com.thinkup.nativead.api.TUNative;
import com.thinkup.nativead.api.TUNativeAdView;
import com.thinkup.nativead.api.TUNativeDislikeListener;
import com.thinkup.nativead.api.TUNativeEventExListener;
import com.thinkup.nativead.api.TUNativeMaterial;
import com.thinkup.nativead.api.TUNativeNetworkListener;
import com.thinkup.nativead.api.TUNativePrepareExInfo;
import com.thinkup.nativead.api.TUNativePrepareInfo;
import com.thinkup.nativead.api.NativeAd;
import com.anythink.reactnative.event.ATReactNativeEventEmitter;
import com.anythink.reactnative.listener.AdListenerFactory;
import com.anythink.reactnative.utils.Const;
import com.anythink.reactnative.utils.MsgTools;
import com.anythink.reactnative.utils.Utils;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
* A placementId Native Helper (supports list of multiple ads).
* Supports TUNative (loader) + multiple NativeAds (indexed by adId, aligned with Android "single TUNative + multiple NativeAd objects").
 *
* adId = Cross-bridge NativeAd object reference: fetchNativeAd is allocated when fetching an item from the cache pool (placementId# serial number),
* JS transparent transmission, no user awareness. Material/rendering/binding/event/life cycle/destruction are all located to the specific one according to adId.
* Empty adId returns to the most recent one (mLastAdId) to ensure that the single ad/transformation half-state does not collapse.
 *
* The callback during the loading period is TUNativeNetworkListener, the callback during the display period is TUNativeEventExListener, and unified emit NativeCall (payload with adId).
 */
public class ATNativeHelper {

    private final Context mAppContext;
    private final String mPlacementId;
    private final Handler mMain = new Handler(Looper.getMainLooper());
    private TUNative mATNative;
    private Map<String, Object> mLocalExtra;
    private Map<String, Object> mTkExtra;
    private String mShowScenarioId;
    private String mShowCustomExt;

    /** Multiple ads indexed by adId (pop order; LinkedHashMap for debugging). */
    private final Map<String, NativeAd> mNativeAds = new LinkedHashMap<>();
    /** Container per ad (views bound by adId, no longer 1:1). */
    private final Map<String, ATNativeContainer> mContainers = new LinkedHashMap<>();
    /** Per-ad template render dedup flag. */
    private final Map<String, Boolean> mTemplateRendered = new HashMap<>();
    /** Self-render renderAdContainer dedup (once per offer; repeat calls clear registered impression/click). */
    private final Map<String, Boolean> mSelfRendered = new HashMap<>();
    /** adId auto-increment sequence. */
    private final AtomicInteger mAdSeq = new AtomicInteger(0);
    /** Most recent adId (fallback when adId is empty). */
    private String mLastAdId;

    /** Empty/unknown adId falls back to most recent; null if none. */
    private String resolveAdId(String adId) {
        if (adId != null && mNativeAds.containsKey(adId)) {
            return adId;
        }
        return mLastAdId;
    }

    private NativeAd adOf(String adId) {
        String id = resolveAdId(adId);
        return id != null ? mNativeAds.get(id) : null;
    }

    /** Fabric ViewManager injects SDK container, bound by adId. */
    public void setContainer(String adId, ATNativeContainer container) {
        String id = resolveAdId(adId);
        if (id != null) {
            mContainers.put(id, container);
        }
    }

    public TUNativeAdView getContainer(String adId) {
        String id = resolveAdId(adId);
        return id != null ? mContainers.get(id) : null;
    }

    public ATNativeHelper(Context appContext, String placementId) {
        mAppContext = appContext;
        mPlacementId = placementId;
    }

    /** Load-phase event (no specific adId; pass null). */
    private void emit(String callbackKey, TUAdInfo adInfo, String errMsg, Map<String, Object> extra) {
        emit(callbackKey, null, adInfo, errMsg, extra);
    }

    /** Show-phase event with adId (route to cell by placementId+adId). */
    private void emit(String callbackKey, String adId, TUAdInfo adInfo, String errMsg, Map<String, Object> extra) {
        Map<String, Object> info = null;
        if (adInfo != null) {
            try {
                info = Utils.jsonStrToMap(adInfo.toString());
            } catch (Throwable ignore) {
            }
        }
        Map<String, Object> ex = extra;
        if (adId != null) {
            ex = (extra != null) ? new HashMap<>(extra) : new HashMap<String, Object>();
            ex.put(Const.CallbackKey.adId, adId);
        }
        ATReactNativeEventEmitter.getInstance().sendCallback(
                Const.CallbackMethodCall.NativeCall, callbackKey, mPlacementId, info, errMsg, ex);
    }

    public void setLocalExtra(Map<String, Object> map) {
        if (map == null || map.isEmpty()) {
            return;
        }
        if (mLocalExtra == null) {
            mLocalExtra = new HashMap<>();
        }
        mLocalExtra.putAll(map);
        if (mATNative != null) {
            mATNative.setLocalExtra(mLocalExtra);
        }
    }

    public void setShowConfig(Map<String, Object> map) {
        if (map == null || map.isEmpty()) {
            mShowScenarioId = "";
            mShowCustomExt = "";
            return;
        }
        Object scenario = map.get("scenarioId");
        Object ext = map.get("showCustomExt");
        mShowScenarioId = scenario != null ? String.valueOf(scenario) : "";
        mShowCustomExt = ext != null ? String.valueOf(ext) : "";
    }

    public void setTKExtra(Map<String, Object> map) {
        mTkExtra = map;
    }

    private TUNative getOrCreate() {
        if (mATNative == null) {
            mATNative = new TUNative(mAppContext, mPlacementId, new TUNativeNetworkListener() {
                @Override
                public void onNativeAdLoaded() {
                    MsgTools.printCallback("Native", "ATNativeHelper", "onNativeAdLoaded",
                            "placementId=" + mPlacementId);
                    emit(Const.NativeCallback.LoadedCallbackKey, null, null, null);
                }

                @Override
                public void onNativeAdLoadFail(AdError adError) {
                    MsgTools.printCallback("Native", "ATNativeHelper", "onNativeAdLoadFail",
                            "placementId=" + mPlacementId + "--error=" + adError.getFullErrorInfo());
                    emit(Const.NativeCallback.LoadFailCallbackKey, null, adError.getFullErrorInfo(), null);
                }
            });
            if (mLocalExtra != null) {
                mATNative.setLocalExtra(mLocalExtra);
            }
            mATNative.setAdMultipleLoadedListener(AdListenerFactory.multipleLoaded(
                    Const.CallbackMethodCall.NativeCall,
                    Const.NativeCallback.MultipleLoadedCallbackKey, mPlacementId));
            mATNative.setAdSourceStatusListener(new TUAdSourceStatusListener() {
                @Override
                public void onAdSourceBiddingAttempt(TUAdInfo adInfo) {
                    MsgTools.printAdSource("ADSource--Bidding--Attempt", "ATNativeHelper", "onAdSourceBiddingAttempt",
                            MsgTools.adSourceParams(mPlacementId, adInfo));
                    emit(Const.NativeCallback.AdSourceBiddingAttemptCallbackKey, adInfo, null, null);
                }

                @Override
                public void onAdSourceBiddingFilled(TUAdInfo adInfo) {
                    MsgTools.printAdSource("ADSource--Bidding--Filled", "ATNativeHelper", "onAdSourceBiddingFilled",
                            MsgTools.adSourceParams(mPlacementId, adInfo));
                    emit(Const.NativeCallback.AdSourceBiddingFilledCallbackKey, adInfo, null, null);
                }

                @Override
                public void onAdSourceBiddingFail(TUAdInfo adInfo, AdError adError) {
                    MsgTools.printAdSource("ADSource--Bidding--Fail", "ATNativeHelper", "onAdSourceBiddingFail",
                            MsgTools.adSourceErrorParams(mPlacementId, adInfo, adError));
                    emit(Const.NativeCallback.AdSourceBiddingFailCallbackKey, adInfo,
                            adError != null ? adError.getFullErrorInfo() : null, null);
                }

                @Override
                public void onAdSourceAttempt(TUAdInfo adInfo) {
                    MsgTools.printAdSource("ADSource--AD--Start", "ATNativeHelper", "onAdSourceAttempt",
                            MsgTools.adSourceParams(mPlacementId, adInfo));
                    emit(Const.NativeCallback.AdSourceAttemptCallbackKey, adInfo, null, null);
                }

                @Override
                public void onAdSourceLoadFilled(TUAdInfo adInfo) {
                    MsgTools.printAdSource("ADSource--AD--Load--Success", "ATNativeHelper", "onAdSourceLoadFilled",
                            MsgTools.adSourceParams(mPlacementId, adInfo));
                    emit(Const.NativeCallback.AdSourceLoadFilledCallbackKey, adInfo, null, null);
                }

                @Override
                public void onAdSourceLoadFail(TUAdInfo adInfo, AdError adError) {
                    MsgTools.printAdSource("ADSource--AD--Load--Fail", "ATNativeHelper", "onAdSourceLoadFail",
                            MsgTools.adSourceErrorParams(mPlacementId, adInfo, adError));
                    emit(Const.NativeCallback.AdSourceLoadFailCallbackKey, adInfo,
                            adError != null ? adError.getFullErrorInfo() : null, null);
                }
            });
        }
        return mATNative;
    }

    public void load(Map<String, Object> settings) {
// settings.atAdRequest parsing (consistent with the three categories); currently minimized: direct makeAdRequest
        getOrCreate().makeAdRequest();
    }

    /**
* Template (express) independent path (attach time adjustment): When the developer has load+getNativeAd and it is express, the SDK automatically renders the template into the container.
* Self-rendering (non-express) does not go here - driven by JS's updateAssetView/renderNativeAd command → renderSelfRender.
     */
    public void renderIfReady(final String adId) {
        final String id = resolveAdId(adId);
        NativeAd ad = id != null ? mNativeAds.get(id) : null;
        if (ad != null && mContainers.get(id) != null && ad.isNativeExpress()) {
            mMain.post(new Runnable() {
                @Override
                public void run() {
                    renderTemplate(id);
                }
            });
        }
    }

    /** Pop one NativeAd from SDK cache; apply JS setShowConfig scenarioId / showCustomExt. */
    private NativeAd pollNativeAdFromCache() {
        TUShowConfig.Builder builder = new TUShowConfig.Builder();
        if (mShowScenarioId != null && mShowScenarioId.length() > 0) {
            builder.scenarioId(mShowScenarioId);
        }
        if (mShowCustomExt != null && mShowCustomExt.length() > 0) {
            builder.showCustomExt(mShowCustomExt);
        }
        return mATNative.getNativeAd(builder.build());
    }

    /** getNativeAd: pop from cache, assign adId, attach show-phase listener. Returns adId (null if no ad). */
    public String fetchNativeAd() {
        if (mATNative == null) {
            return null;
        }
        NativeAd nativeAd = pollNativeAdFromCache();
        MsgTools.printMsg("fetchNativeAd: " + mPlacementId + ", hasAd=" + (nativeAd != null));
        if (nativeAd == null) {
            return null;
        }
        final String adId = mPlacementId + "#" + mAdSeq.incrementAndGet();
        mNativeAds.put(adId, nativeAd);
        mLastAdId = adId;
        nativeAd.setAdRevenueListener(AdListenerFactory.revenueListener());
        nativeAd.setAdDownloadListener(AdListenerFactory.downloadListener());
        nativeAd.setDislikeCallbackListener(new TUNativeDislikeListener() {
            @Override
            public void onAdCloseButtonClick(TUNativeAdView view, TUAdInfo atAdInfo) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onAdCloseButtonClick",
                        "placementId=" + mPlacementId + "--adId=" + adId + "--adInfo=" + atAdInfo);
                emit(Const.NativeCallback.CloseCallbackKey, adId, atAdInfo, null, null);
            }
        });
        nativeAd.setNativeEventListener(new TUNativeEventExListener() {
            @Override
            public void onAdImpressed(TUNativeAdView view, TUAdInfo atAdInfo) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onAdImpressed",
                        "placementId=" + mPlacementId + "--adId=" + adId + "--adInfo=" + atAdInfo);
                emit(Const.NativeCallback.ShowCallbackKey, adId, atAdInfo, null, null);
            }

            @Override
            public void onAdClicked(TUNativeAdView view, TUAdInfo atAdInfo) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onAdClicked",
                        "placementId=" + mPlacementId + "--adId=" + adId + "--adInfo=" + atAdInfo);
                emit(Const.NativeCallback.ClickCallbackKey, adId, atAdInfo, null, null);
            }

            @Override
            public void onAdVideoStart(TUNativeAdView view) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onAdVideoStart",
                        "placementId=" + mPlacementId + "--adId=" + adId);
                emit(Const.NativeCallback.VideoStartKey, adId, null, null, null);
            }

            @Override
            public void onAdVideoEnd(TUNativeAdView view) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onAdVideoEnd",
                        "placementId=" + mPlacementId + "--adId=" + adId);
                emit(Const.NativeCallback.VideoEndKey, adId, null, null, null);
            }

            @Override
            public void onAdVideoProgress(TUNativeAdView view, int progress) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onAdVideoProgress",
                        "placementId=" + mPlacementId + "--adId=" + adId + "--progress=" + progress);
                Map<String, Object> extra = new HashMap<>();
                extra.put("progress", progress);
                emit(Const.NativeCallback.VideoProgressKey, adId, null, null, extra);
            }

            @Override
            public void onDeeplinkCallback(TUNativeAdView atNativeAdView, TUAdInfo atAdInfo, boolean isSuccess) {
                MsgTools.printCallback("Native", "ATNativeHelper", "onDeeplinkCallback",
                        "placementId=" + mPlacementId + "--adId=" + adId + "--adInfo=" + atAdInfo
                                + "--isSuccess=" + isSuccess);
                Map<String, Object> extra = new HashMap<>();
                extra.put(Const.CallbackKey.isDeeplinkSuccess, isSuccess);
                emit(Const.NativeCallback.DeeplinkCallbackKey, adId, atAdInfo, null, extra);
            }
        });
        return adId;
    }

    public NativeAd getNativeAd(String adId) {
        return adOf(adId);
    }

    /**
* Self-rendering material return: take the material field from NativeAd.getAdMaterial() and package it to JS,
* The value is self-rendered by RN's AssetView component (&lt;Text&gt;/&lt;Image&gt;). The key uses Const.Native.*.
* mediaView is the SDK native view (cannot be serialized), and only returns the isMediaViewAvailable flag. The true view is addView when it is renderNativeAd.
     */
    public Map<String, Object> getAdMaterialMap(String adId) {
        NativeAd ad = adOf(adId);
        if (ad == null) {
            return null;
        }
        TUNativeMaterial m = ad.getAdMaterial();
        if (m == null) {
            return null;
        }
        boolean hasMedia = m.getAdMediaView() != null;
        boolean express = ad.isNativeExpress();
        Map<String, Object> map = new HashMap<>();
        map.put(Const.Native.title, m.getTitle());
        map.put(Const.Native.desc, m.getDescriptionText());
        map.put(Const.Native.cta, m.getCallToActionText());
        map.put(Const.Native.icon, m.getIconImageUrl());
        map.put(Const.Native.mainImage, m.getMainImageUrl());
        Double star = m.getStarRating();
        if (star != null) {
            map.put("starRating", star);
        }
        map.put("advertiser", m.getAdvertiserName());
        map.put("isMediaViewAvailable", hasMedia);
// offer Type: express(template) → JS hides AssetView and lets SDK draw the template
        map.put("isExpress", express);
        MsgTools.printMsg("getAdMaterial: " + mPlacementId + ", express=" + express + ", hasMedia=" + hasMedia);
        return map;
    }

    public boolean isNativeExpress(String adId) {
        NativeAd ad = adOf(adId);
        return ad != null && ad.isNativeExpress();
    }

    public Map<String, Object> checkAdStatus() {
        Map<String, Object> map = new HashMap<>(5);
        if (mATNative != null) {
            com.thinkup.core.api.TUAdStatusInfo info = mATNative.checkAdStatus();
            map.put("isLoading", info.isLoading());
            map.put("isReady", info.isReady());
            TUAdInfo top = info.getTUTopAdInfo();
            if (top != null) {
                map.put("adInfo", top.toString());
            }
            return map;
        }
        map.put("isLoading", false);
        map.put("isReady", false);
        return map;
    }

    public List<TUAdInfo> validAdCaches() {
        return mATNative != null ? mATNative.checkValidAdCaches() : null;
    }

    /**
* Template (express) independent path: SDK uses renderAdContainer to render the platform template into the container and then prepare.
* Only express goes (renderIfReady or renderSelfRender forward); self-rendering goes doRenderSelfRender.
* The template view is injected by the SDK, and the Fabric container will not layout it → force measure+layout to fill the container after rendering (the same method as the template black block).
     */
    public void renderTemplate(String adId) {
        String id = resolveAdId(adId);
        if (id == null) {
            return;
        }
        NativeAd ad = mNativeAds.get(id);
        ATNativeContainer container = mContainers.get(id);
        if (ad == null || container == null || !ad.isNativeExpress()) {
            return;
        }
        if (Boolean.TRUE.equals(mTemplateRendered.get(id))) {
return; // Already rendered to avoid duplication renderAdContainer
        }
        mTemplateRendered.put(id, true);
        MsgTools.printMsg("renderTemplate: " + mPlacementId + ", adId=" + id);
        try {
            ad.renderAdContainer(container, null);
        } catch (Throwable e) {
            MsgTools.printMsg("renderAdContainer fail: " + e.getMessage());
        }
        ad.prepare(container, new TUNativePrepareExInfo());
        forceLayoutNativeChild(container, /*wrapHeight*/ true);
    }

    /**
* SDK-injected views (template views / media surfaces) are not laid out by Yoga → 0 size in the Fabric container.
* Fixed with host size measure+layout for ~30 frames in host's OnPreDraw (allowing the asynchronous surface to be laid out after it is in place).
     *
* @param host listening + container providing size
* @param wrapHeight true (template): Use the host width EXACTLY + height UNSPECIFIED to measure the natural height of the SDK view, and support the host to this height;
* false (media): Directly use the host width and height EXACTLY to cover it.
     */
    private void forceLayoutNativeChild(final android.view.ViewGroup host, final boolean wrapHeight) {
        final int[] frames = {0};
        host.getViewTreeObserver().addOnPreDrawListener(
                new android.view.ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        int w = host.getWidth();
                        int h = host.getHeight();
                        if (frames[0]++ > 30 || w <= 0 || (!wrapHeight && h <= 0)) {
                            android.view.ViewTreeObserver o = host.getViewTreeObserver();
                            if (o.isAlive()) {
                                o.removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                        int wSpec = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY);
                        int hSpec = wrapHeight
                                ? View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
                                : View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY);
                        int maxH = 0;
                        for (int i = 0; i < host.getChildCount(); i++) {
                            View child = host.getChildAt(i);
//Only layout the views injected by the SDK, skipping RN's ReactXxx (they are laid out by Yoga themselves)
                            if (child.getClass().getName().startsWith("com.facebook.react")) {
                                continue;
                            }
                            child.measure(wSpec, hSpec);
                            int mh = wrapHeight ? child.getMeasuredHeight() : h;
                            child.layout(0, 0, w, mh > 0 ? mh : h);
                            maxH = Math.max(maxH, mh);
                        }
// Template: Prop the host to the height of the SDK view (host=ReactViewGroup, otherwise it will collapse and not be fully visible)
                        if (wrapHeight && maxH > 0 && host.getHeight() < maxH) {
                            host.setMinimumHeight(maxH);
                            android.view.ViewGroup.LayoutParams lp = host.getLayoutParams();
                            if (lp != null && lp.height != maxH) {
                                lp.height = maxH;
                                host.setLayoutParams(lp);
                            }
                        }
                        return true;
                    }
                });
    }

    /**
* Self-rendering binding (triggered by JS's renderNativeAd command, debounce 50ms to prevent multiple jitters).
* Process: findViewById (each asset tag) → setXxxView + receive clickViewList; getAdMediaView addView enters mediaView placeholder;
* mNativeAd.prepare register clicks/exposures. RN view is laid out by RN itself, no re-parent → no relayout requirement.
     *
* @param container ATNativeContainer (=TUNativeAdView subclass)
* @param assetTags JS registered {react tag → type}
     */
    public void renderSelfRender(final String adId,
                                 final ATNativeContainer container,
                                 final Map<Integer, String> assetTags,
                                 final Map<Integer, Integer> assetColors) {
        final String id = resolveAdId(adId);
        if (id == null) {
            return;
        }
// Press adId debounce (multiple cells are rendered independently and do not cancel each other)
        Runnable prev = mRenderRunnables.remove(id);
        if (prev != null) {
            mMain.removeCallbacks(prev);
        }
        Runnable r = new Runnable() {
            @Override
            public void run() {
                mRenderRunnables.remove(id);
                doRenderSelfRender(id, container, assetTags, assetColors);
            }
        };
        mRenderRunnables.put(id, r);
        mMain.postDelayed(r, 50);
    }

    private final Map<String, Runnable> mRenderRunnables = new HashMap<>();

    private void doRenderSelfRender(String adId,
                                    ATNativeContainer container,
                                    Map<Integer, String> assetTags,
                                    Map<Integer, Integer> assetColors) {
        NativeAd ad = adOf(adId);
        if (ad == null || container == null || assetTags == null) {
            return;
        }
        if (ad.isNativeExpress()) {
// This bit auction returns the template offer → Convert the template path (SDK draws the template), ignoring the AssetView.
// The JS side will also hide the AssetView according to the isExpress of getNativeAd.
            MsgTools.printMsg("renderSelfRender: express offer, route to renderTemplate: " + mPlacementId);
            renderTemplate(adId);
            return;
        }
final NativeAd mNativeAd = ad; // The original variable name will be used below
        MsgTools.printMsg("renderSelfRender: " + mPlacementId + ", adId=" + adId + ", assets=" + assetTags.size());

// Model A: Wait until the RN AssetView is laid out (with dimensions) and then rebuild it natively to avoid missing materials when measure is measured to 0.
// OnPreDraw Wait until the key material has dimensions (or timeout ~30 frames) before executing doRebuildAndRegister.
        scheduleRebuildWhenLaidOut(adId, mNativeAd, container, assetTags, assetColors);
    }

    /** Rebuild+register after RN AssetView layout (key assets sized); also run on timeout (best effort with current sizes). */
    private void scheduleRebuildWhenLaidOut(final String adId, final NativeAd nativeAd,
                                            final ATNativeContainer container,
                                            final Map<Integer, String> assetTags,
                                            final Map<Integer, Integer> assetColors) {
        final int[] frames = {0};
        container.getViewTreeObserver().addOnPreDrawListener(
                new android.view.ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
// Wait for the "key material" to have size before rebuilding: the title/icon must have content. If they have size, it means that the RN layout is completed.
//Do not check all assets——media/mainImage According to offer, choose one of the two, the empty one will collapse to 0 height (collapsed),
// If they are required to be >0, then allSized will always be false → must time out and be forced to move → the other assets will not be good at that moment → blank.
                        boolean keySized = false;
                        for (Map.Entry<Integer, String> e : assetTags.entrySet()) {
                            if (e.getKey() == null) {
                                continue;
                            }
                            String type = e.getValue();
                            if (Const.Native.title.equals(type) || Const.Native.icon.equals(type)) {
                                View v = container.findViewById(e.getKey());
                                if (v != null && v.getWidth() > 0 && v.getHeight() > 0) {
                                    keySized = true;
                                    break;
                                }
                            }
                        }
                        if (keySized || frames[0]++ > 60) {
                            android.view.ViewTreeObserver o = container.getViewTreeObserver();
                            if (o.isAlive()) {
                                o.removeOnPreDrawListener(this);
                            }
                            doRebuildAndRegister(adId, nativeAd, container, assetTags, assetColors);
                        }
                        return true;
                    }
                });
    }

    /** Native rebuild assets (incl. media) + renderAdContainer + prepare + forced layout. */
    private void doRebuildAndRegister(String adId, NativeAd nativeAd,
                                      ATNativeContainer container,
                                      Map<Integer, String> assetTags,
                                      Map<Integer, Integer> assetColors) {
// Guard: OnPreDraw is triggered asynchronously, the cell may have rolled out/onDrop (destroy clears mNativeAds, container is unbound).
// At this time, adId is no longer in the cache or the container has been changed to another adId, then the reconstruction will be abandoned to avoid using the abandoned object renderAdContainer/prepare.
        String id = resolveAdId(adId);
        if (id == null || mNativeAds.get(id) != nativeAd || mContainers.get(id) != container) {
            MsgTools.printMsg("rebuild abort (cell dropped/rebound): " + adId);
            return;
        }
// media: SDK native view, hand it over to the rebuilder and put it into the nativeRoot (no more RN holder - Model A uses the reconstruction layer entirely).
        View mediaView = null;
        try {
            TUNativeMaterial m = nativeAd.getAdMaterial();
            mediaView = (m != null && m.getAdMediaView() != null) ? m.getAdMediaView(container) : null;
        } catch (Throwable e) {
            MsgTools.printMsg("getAdMediaView fail: " + e.getMessage());
        }

// Remove the last reconstruction layer (when re-rendering/changing offer, otherwise multiple sets of materials will overlap and ghost).
        View old = container.findViewWithTag(NativeSelfRenderRebuilder.REBUILD_ROOT_TAG);
        if (old != null) {
            container.removeView(old);
        }

        TUNativePrepareInfo info = new TUNativePrepareExInfo();
        java.util.List<View> clickViews = new java.util.ArrayList<>();
        TUNativeMaterial material = nativeAd.getAdMaterial();
        android.widget.FrameLayout nativeRoot = NativeSelfRenderRebuilder.build(
                container, material, assetTags, assetColors, mediaView, info, clickViews);

// renderAdContainer (render→prepare sequence, once every offer) allows exclusive container ADNs such as GDT to be registered successfully.
        if (!Boolean.TRUE.equals(mSelfRendered.get(adId))) {
            mSelfRendered.put(adId, true);
            try {
                nativeAd.renderAdContainer(container, nativeRoot);
            } catch (Throwable e) {
                MsgTools.printMsg("renderAdContainer(self) fail: " + e.getMessage());
            }
        }
        nativeAd.prepare(container, info);

// Hide the AssetView rendered by RN (replaced by the native reconstruction layer), otherwise the two sets will coexist and be misaligned when the layout changes → ghosting.
// The nesting level of AssetView is variable (grandchildren/great-grandchildren) and cannot be skipped precisely by tag; change to selfRenderActive:
// container.drawChild only draws the native reconstruction layer and skips all RN subtrees (setVisibility will be overwritten by Fabric and is unavailable).
        container.selfRenderActive = true;
        container.invalidate();

// The reconstruction layer must be forced to measure/layout in the RN container to be visible (tool that skips RN child nodes).
        forceLayoutNativeChild(container, /*wrapHeight*/ false);
    }

    /** Destroy one ad (empty adId → most recent; no-op if id already destroyed; do not fall back mLastAdId and kill other cells). */
    public void destroy(String adId) {
        final String id;
        if (adId == null || adId.isEmpty()) {
            id = mLastAdId;
        } else if (mNativeAds.containsKey(adId)) {
            id = adId;
        } else {
            MsgTools.printMsg("native destroy skip (already gone): " + mPlacementId + ", adId=" + adId);
            return;
        }
        if (id == null) {
            return;
        }
        MsgTools.printMsg("native destroy: " + mPlacementId + ", adId=" + id);
        NativeAd ad = mNativeAds.remove(id);
        ATNativeContainer container = mContainers.remove(id);
        mTemplateRendered.remove(id);
        mSelfRendered.remove(id);
        Runnable r = mRenderRunnables.remove(id);
        if (r != null) {
            mMain.removeCallbacks(r);
        }
        if (id.equals(mLastAdId)) {
            mLastAdId = null;
        }
        if (container != null) {
            container.selfRenderActive = false;
            container.invalidate();
        }
        if (ad != null) {
// ad.destory() will trigger GDT internal ExoPlayer.stop - must be the main thread (media3 thread check).
// The JS point removes the RN bridge thread here, so the post main thread is forced to avoid "wrong thread" crashes.
            final NativeAd toDestroy = ad;
            runOnMain(new Runnable() {
                @Override
                public void run() {
                    toDestroy.destory();
                }
            });
        }
    }

    /** Run on main thread (run directly if already on main). */
    private void runOnMain(Runnable r) {
        if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
            r.run();
        } else {
            mMain.post(r);
        }
    }

    /** Destroy entire placement (clear all ads + drain SDK cache pool). */
    public void destroyAll() {
        MsgTools.printMsg("native destroyAll: " + mPlacementId);
        final java.util.List<NativeAd> ads = new java.util.ArrayList<>(mNativeAds.values());
        runOnMain(new Runnable() {
            @Override
            public void run() {
                for (NativeAd ad : ads) {
                    if (ad != null) {
                        ad.destory();
                    }
                }
            }
        });
        mNativeAds.clear();
        mContainers.clear();
        mTemplateRendered.clear();
        mSelfRendered.clear();
        for (Runnable r : mRenderRunnables.values()) {
            mMain.removeCallbacks(r);
        }
        mRenderRunnables.clear();
        mLastAdId = null;
        if (mATNative != null) {
            NativeAd cached;
            while ((cached = mATNative.getNativeAd()) != null) {
                final NativeAd toDestroy = cached;
                runOnMain(new Runnable() {
                    @Override
                    public void run() {
                        toDestroy.destory();
                    }
                });
            }
            mATNative = null;
        }
    }

    /** Lifecycle: resume one ad (empty adId → most recent). Video pause/resume, impression timing depend on ad source. */
    public void onResume(String adId) {
        NativeAd ad = adOf(adId);
        if (ad != null) {
            ad.onResume();
        }
    }

    /** Lifecycle: pause one ad. */
    public void onPause(String adId) {
        NativeAd ad = adOf(adId);
        if (ad != null) {
            ad.onPause();
        }
    }
}
