using System;
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.Video;

namespace IndieExchange
{
    internal sealed class AdView : MonoBehaviour
    {
        private PreparedAd ad;
        private Action opened, clicked, closed, rewarded;
        private RewardPlayback progress;
        private bool isRewarded, confirming, completed, videoSuspended;
        private GameObject confirmation;
        private double sampleTime, samplePosition, stalledSince, presentedPosition;
        private RectTransform safe;
        private bool banner, ending, notified, paused;
        private double visibleSince;
        private GameObject ownEventSystem;
        private Text status;
        private RawImage artwork;
        private AspectRatioFitter artworkFit;
        private bool Foreground {
            get {
#if UNITY_EDITOR
                if (IndieAds.Runner != null && IndieAds.Runner.TestForeground) return true;
#endif
                return Application.isFocused || ad.test && Application.isBatchMode;
            }
        }

        public static AdView Create(Transform parent, PreparedAd ad, IndiePlacement placement, Action opened, Action clicked, Action closed, Action rewarded = null)
        {
            var go = new GameObject("Indie Exchange Ad", typeof(RectTransform));
            go.transform.SetParent(parent, false);
            var view = go.AddComponent<AdView>();
            try { view.Build(ad, placement, opened, clicked, closed, rewarded); return view; }
            catch { view.closed = null; Destroy(go); throw; }
        }

        private void Build(PreparedAd source, IndiePlacement placement, Action onOpen, Action onClick, Action onClose, Action onReward)
        {
            ad = source; opened = onOpen; clicked = onClick; closed = onClose;
            ad.RefreshTestArtwork(Screen.width, Screen.height);
            banner = placement.format == IndieAdFormat.Banner;
            isRewarded = placement.format == IndieAdFormat.Rewarded;
            rewarded = onReward;
            if (isRewarded) {
                if (ad.video == null && !ad.test) throw new InvalidOperationException("Rewarded ads require video.");
                progress = new RewardPlayback(ad.video != null ? ad.video.length : 3);
            }
            var canvas = gameObject.AddComponent<Canvas>();
            canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            canvas.sortingOrder = 32760;
            var scaler = gameObject.AddComponent<CanvasScaler>();
            scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
            scaler.referenceResolution = new Vector2(1080, 1920);
            scaler.matchWidthOrHeight = .5f;
            gameObject.AddComponent<GraphicRaycaster>();
            EnsureEvents();
            if (!banner)
            {
                var background = Panel(transform, "Background", new Color(.035f,.05f,.075f,1));
                Stretch(background);
            }
            safe = new GameObject("Safe area", typeof(RectTransform)).GetComponent<RectTransform>();
            safe.SetParent(transform, false);
            UpdateSafeArea();
            if (banner)
            {
                // Fixed logical slot scaled to fit the safe width; never crop the creative.
                var dimensions = placement.bannerSize.Split('x');
                var width = float.Parse(dimensions[0], System.Globalization.CultureInfo.InvariantCulture);
                var height = float.Parse(dimensions[1], System.Globalization.CultureInfo.InvariantCulture);
                var panel = Panel(safe, "Banner", Color.black);
                var edge = placement.bannerPosition == "top" ? 1f : 0f;
                panel.anchorMin = panel.anchorMax = new Vector2(.5f, edge);
                panel.pivot = new Vector2(.5f, edge);
                panel.sizeDelta = new Vector2(width, height);
                var layout = panel.gameObject.AddComponent<BannerLayout>();
                layout.logicalSize = new Vector2(width, height);
                layout.safe = safe;
                var raw = Image(panel, ad.texture); Stretch(raw.rectTransform);
                raw.raycastTarget = true;
                var button = raw.gameObject.AddComponent<Button>(); button.onClick.AddListener(Click);
                // Built-in artwork already has a prominent test label. Server previews
                // must retain the SDK label regardless of what their creative contains.
                if (!ad.UsesBuiltInArtwork) {
                    var label = Label(panel, ad.test ? "TEST AD" : "Ad", 12, TextAnchor.UpperLeft);
                    label.color = Color.white;
                    label.gameObject.AddComponent<Outline>();
                    Stretch(label.rectTransform);
                }
            }
            else
            {
                var title = Label(safe, ad.test ? "INDIE EXCHANGE  /  TEST AD" : "ADVERTISEMENT", 25, TextAnchor.MiddleLeft);
                Box(title.rectTransform, new Vector2(.04f,.92f), new Vector2(.77f,.99f));
                var close = Button(safe, "Close", RequestClose);
                Box(close, new Vector2(.79f,.92f), new Vector2(.97f,.985f));
                var content = new GameObject("Creative area", typeof(RectTransform)).GetComponent<RectTransform>();
                content.SetParent(safe, false); Box(content, new Vector2(.03f,.19f), new Vector2(.97f,.9f));
                var raw = Image(content, ad.texture);
                var fit = raw.gameObject.AddComponent<AspectRatioFitter>();
                fit.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
                fit.aspectRatio = (float)ad.texture.width / ad.texture.height;
                artwork = raw; artworkFit = fit;
                var headline = Label(safe, ad.response.headline ?? ad.response.gameName, 35, TextAnchor.MiddleCenter);
                Box(headline.rectTransform, new Vector2(.04f,.12f), new Vector2(.96f,.19f));
                var install = Button(safe, ad.test ? "Test install button" : string.IsNullOrWhiteSpace(ad.response.ctaText) ? "Install" : ad.response.ctaText, Click);
                Box(install, new Vector2(.12f,.035f), new Vector2(.88f,.11f));
                status = Label(safe, ad.test ? "Preview only. No credits or tracking." : "", 20, TextAnchor.MiddleCenter);
                Box(status.rectTransform, new Vector2(.04f,0), new Vector2(.96f,.035f));
            }
            visibleSince = Time.realtimeSinceStartupAsDouble;
            if (ad.video != null)
            {
                if (ad.audio != null) ad.audio.mute = false;
                if (!isRewarded) ad.video.time = 0;
                if (isRewarded) {
                    ad.video.loopPointReached += VideoEnded;
                    ad.video.sendFrameReadyEvents = true;
                    ad.video.frameReady += VideoFrameReady;
                    ad.video.seekCompleted += UnexpectedSeek;
                }
                ad.video.Play();
            }
            sampleTime = stalledSince = Time.realtimeSinceStartupAsDouble;
            StartCoroutine(Visible());
        }

        private IEnumerator Visible()
        {
            yield return null;
            while (!ending && !notified)
            {
                if (!paused && Foreground && (!banner || Time.realtimeSinceStartupAsDouble - visibleSince >= 1))
                {
                    notified = true;
                    opened?.Invoke();
                }
                yield return null;
            }
        }

        private void Update()
        {
            if (ending) return;
            UpdateSafeArea();
            if (ad != null && ad.failed) { Close(); return; }
            if (!banner && ad.RefreshTestArtwork(Screen.width, Screen.height)) {
                artwork.texture = ad.texture;
                artworkFit.aspectRatio = (float)ad.texture.width / ad.texture.height;
            }
            if (!banner)
            {
#if ENABLE_LEGACY_INPUT_MANAGER || !ENABLE_INPUT_SYSTEM
                if (Input.GetKeyDown(KeyCode.Escape)) RequestClose();
#endif
            }
            if (paused || !Foreground) visibleSince = Time.realtimeSinceStartupAsDouble;
            SyncVideo();
            if (isRewarded && !completed && !ending) {
                SampleReward(false);
                status.text = "Watch to earn your reward · " + Math.Max(0, (int)Math.Ceiling(progress.Duration - progress.Watched)) + "s" +
                    (ad.test ? " · TEST" : "");
                // A failed/stalled decoder must not leave the game stuck behind this overlay.
                if (!paused && Foreground && !confirming && Time.realtimeSinceStartupAsDouble - stalledSince > 15) Close();
            }
        }

        private void OnApplicationPause(bool value)
        {
            paused = value;
            visibleSince = Time.realtimeSinceStartupAsDouble;
            SyncVideo();
        }

        private void OnApplicationFocus(bool focused) { if (!focused && ad?.video != null) { ad.video.Pause(); videoSuspended = true; } }

        private void SyncVideo()
        {
            if (ending || ad?.video == null) return;
            var suspend = paused || !Foreground || confirming;
            if (suspend && !videoSuspended) { ad.video.Pause(); videoSuspended = true; }
            else if (!suspend && videoSuspended && !completed) { ad.video.Play(); videoSuspended = false; }
        }

        private void SampleReward(bool ended)
        {
            if (completed || ending || progress == null) return;
            var now = Time.realtimeSinceStartupAsDouble;
            var active = !paused && Foreground && !confirming && !ad.failed;
            var position = ad.video != null ? (ended ? ad.video.length : presentedPosition) :
                samplePosition + (active ? Math.Max(0, now - sampleTime) : 0);
            if (position > samplePosition || !active) stalledSince = now;
            if (progress.Sample(position, now - sampleTime, active, ended || ad.video == null && position >= progress.Duration)) {
                completed = true;
                status.text = ad.test ? "TEST · Reward earned. Close to continue." : "Reward earned. Close to continue.";
                rewarded?.Invoke();
            }
            sampleTime = now; samplePosition = position;
        }

        private void VideoEnded(VideoPlayer player) { SampleReward(true); if (!completed) Close(); }

        private void UnexpectedSeek(VideoPlayer player) { progress.Invalidate(); Close(); }

        private void VideoFrameReady(VideoPlayer player, long frame)
        {
            // Track frames actually presented. Some native decoders leave time stale between seeks.
            if (player.frameRate > 0) presentedPosition = Math.Min(progress.Duration, Math.Max(0, frame / player.frameRate));
        }

        internal void RequestClose()
        {
            if (!isRewarded || completed) { Close(); return; }
            if (confirming || ending) return;
            confirming = true; SyncVideo();
            confirmation = Panel(safe, "Leave reward confirmation", new Color(.035f,.05f,.075f,.98f)).gameObject;
            Stretch((RectTransform)confirmation.transform);
            var label = Label(confirmation.transform, "Leave without your reward?", 36, TextAnchor.MiddleCenter);
            Box(label.rectTransform, new Vector2(.06f,.52f), new Vector2(.94f,.67f));
            Box(Button(confirmation.transform, "Keep watching", ContinueWatching), new Vector2(.12f,.4f), new Vector2(.88f,.49f));
            Box(Button(confirmation.transform, "Leave without reward", Close), new Vector2(.12f,.27f), new Vector2(.88f,.36f));
        }

        internal void ContinueWatching()
        {
            if (confirmation != null) Destroy(confirmation);
            confirming = false; sampleTime = stalledSince = Time.realtimeSinceStartupAsDouble;
            samplePosition = ad.video != null ? presentedPosition : samplePosition;
            progress?.Sample(samplePosition, 0, false, false);
            SyncVideo();
        }

        private void UpdateSafeArea()
        {
            if (safe == null || Screen.width == 0 || Screen.height == 0) return;
            var area = Screen.safeArea;
            safe.anchorMin = new Vector2(area.xMin / Screen.width, area.yMin / Screen.height);
            safe.anchorMax = new Vector2(area.xMax / Screen.width, area.yMax / Screen.height);
            safe.offsetMin = safe.offsetMax = Vector2.zero;
        }

        private void Click()
        {
            if (ad.test) { if (status != null) status.text = "Install button works. Live ads open the store."; return; }
            if (!AdTransport.StoreUrl(ad.response.destinationUrl)) return;
            // Preserve impression-before-click ordering even if tapped immediately.
            if (!notified && !banner) { notified = true; opened?.Invoke(); }
            if (notified) clicked?.Invoke();
            Application.OpenURL(ad.response.destinationUrl);
        }

        internal void Close()
        {
            if (ending) return;
            ending = true;
            if (ad?.video != null) { ad.video.loopPointReached -= VideoEnded; ad.video.frameReady -= VideoFrameReady; ad.video.seekCompleted -= UnexpectedSeek; }
            gameObject.SetActive(false);
            closed?.Invoke();
            Destroy(gameObject);
        }
        private void OnDestroy()
        {
            if (ad?.video != null) { ad.video.loopPointReached -= VideoEnded; ad.video.frameReady -= VideoFrameReady; ad.video.seekCompleted -= UnexpectedSeek; }
            if (ownEventSystem != null) Destroy(ownEventSystem);
            if (!ending) { ending = true; closed?.Invoke(); }
        }

        private void EnsureEvents()
        {
            if (EventSystem.current != null) return;
            ownEventSystem = new GameObject("Indie Exchange UI input", typeof(EventSystem));
            ownEventSystem.transform.SetParent(transform, false);
#if ENABLE_INPUT_SYSTEM
            var type = Type.GetType("UnityEngine.InputSystem.UI.InputSystemUIInputModule, Unity.InputSystem");
            if (type == null) throw new InvalidOperationException("Input System UI module is missing.");
            ownEventSystem.AddComponent(type);
#else
            ownEventSystem.AddComponent<StandaloneInputModule>();
#endif
        }

        private static RectTransform Panel(Transform parent, string name, Color color)
        {
            var go = new GameObject(name, typeof(RectTransform), typeof(Image));
            go.transform.SetParent(parent, false); go.GetComponent<Image>().color = color;
            return go.GetComponent<RectTransform>();
        }
        private static RawImage Image(Transform parent, Texture texture)
        {
            var go = new GameObject("Artwork", typeof(RectTransform), typeof(RawImage));
            go.transform.SetParent(parent, false);
            var image = go.GetComponent<RawImage>(); image.texture = texture; image.raycastTarget = false;
            return image;
        }
        private static Text Label(Transform parent, string text, int size, TextAnchor align)
        {
            var go = new GameObject("Label", typeof(RectTransform), typeof(Text)); go.transform.SetParent(parent, false);
            var label = go.GetComponent<Text>(); label.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
            label.text = text; label.fontSize = size; label.alignment = align; label.color = Color.white; label.raycastTarget = false;
            label.supportRichText = false;
            label.resizeTextForBestFit = true; label.resizeTextMinSize = Math.Max(10, size / 2); label.resizeTextMaxSize = size;
            return label;
        }
        private static RectTransform Button(Transform parent, string text, UnityEngine.Events.UnityAction action)
        {
            var rect = Panel(parent, text, new Color(.12f,.48f,.39f));
            var button = rect.gameObject.AddComponent<Button>(); button.onClick.AddListener(action);
            var label = Label(rect, text, 28, TextAnchor.MiddleCenter);
            Stretch(label.rectTransform);
            return rect;
        }
        private static void Stretch(RectTransform rect) => Box(rect, Vector2.zero, Vector2.one);
        private static void Box(RectTransform rect, Vector2 min, Vector2 max)
        {
            rect.anchorMin = min; rect.anchorMax = max; rect.offsetMin = rect.offsetMax = Vector2.zero;
        }
    }

    internal sealed class BannerLayout : MonoBehaviour
    {
        public RectTransform safe;
        public Vector2 logicalSize;
        private void LateUpdate()
        {
            if (safe == null) return;
            // Match a dp-style logical slot, using 160 dpi when the device reports no density.
            var canvas = GetComponentInParent<Canvas>();
            var density = Screen.dpi > 0 ? Mathf.Clamp(Screen.dpi / 160f, 1, 4) : 1;
            var scale = Mathf.Min(density / canvas.scaleFactor, safe.rect.width / logicalSize.x, safe.rect.height / logicalSize.y);
            ((RectTransform)transform).sizeDelta = logicalSize * scale;
        }
    }
}
