using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Video;

namespace IndieExchange
{
    internal sealed class PreparedAd : IDisposable
    {
        public AdResponse response;
        public Texture texture;
        public VideoPlayer video;
        public AudioSource audio;
        public string file;
        public bool test, failed;
        public double expires;
        private IndiePlacement testPlacement;
        private string testArtworkKey;
        internal bool UsesBuiltInArtwork => testPlacement != null;
        public bool Ready => !failed && texture != null && Time.realtimeSinceStartupAsDouble < expires &&
            (video == null || video.isPrepared);
        public void Dispose()
        {
            if (video != null) { video.Stop(); UnityEngine.Object.Destroy(video.gameObject); video = null; }
            if (texture != null) { if (texture is RenderTexture rt) rt.Release(); UnityEngine.Object.Destroy(texture); texture = null; }
            try { if (!string.IsNullOrEmpty(file) && File.Exists(file)) File.Delete(file); } catch { }
        }

        public static PreparedAd Test(IndiePlacement placement, int minutes)
        {
            var ad = new PreparedAd { test = true, testPlacement = placement, expires = Time.realtimeSinceStartupAsDouble + minutes * 60,
                response = new AdResponse { requestId = Guid.NewGuid().ToString("N"), gameName = "Indie Exchange test ad",
                    headline = placement.format == IndieAdFormat.Rewarded ? "Rewarded test ad" : "Interstitial test ad", ctaText = "Test install button",
                    creative = new AdCreative { type = 0 } } };
            ad.RefreshTestArtwork(Screen.width, Screen.height);
            return ad;
        }

        internal bool RefreshTestArtwork(int width, int height)
        {
            if (!UsesBuiltInArtwork) return false;
            var key = testPlacement.format == IndieAdFormat.Banner ? "banner-" + testPlacement.bannerSize :
                width > height ? "landscape" : "portrait";
            if (key == testArtworkKey && texture != null) return false;
            var asset = Resources.Load<TextAsset>("IndieExchange/TestAds/" + key);
            var replacement = new Texture2D(2, 2, TextureFormat.RGB24, false) {
                name = "Indie Exchange test " + key, filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp
            };
            if (asset == null || !ImageConversion.LoadImage(replacement, asset.bytes, true)) {
                UnityEngine.Object.Destroy(replacement);
                if (texture == null) throw new InvalidOperationException("Indie Exchange test artwork is missing. Reimport the complete SDK package.");
                return false;
            }
            if (texture != null) UnityEngine.Object.Destroy(texture);
            texture = replacement; testArtworkKey = key;
            return true;
        }

        public static async Task<PreparedAd> Load(IndieAdsRunner runner, IndiePlacement placement, IndieAdsSettings settings,
            string folder, string installId, bool http, CancellationToken token)
        {
            PreparedAd ad = null;
            runner.LoadError = "Loading creative...";
            try
            {
                var requestId = Guid.NewGuid().ToString("N");
                var platform = Application.platform == RuntimePlatform.IPhonePlayer ? "ios" :
                    Application.isEditor ? settings.editorPlatform : "android";
                var request = new AdRequest { sdkKey = settings.sdkKey, requestId = requestId, platform = platform,
                    countryCode = settings.countryCode, deviceHash = installId, format = (int)placement.format,
                    session = settings.session, category = placement.category,
                    bannerSize = placement.format == IndieAdFormat.Banner ? placement.bannerSize : null };
                var result = await SecureTransport.Send(settings, installId, "ad-request", JsonUtility.ToJson(request), token);
                if (result.status == 429) runner.DeferLoads(placement.format, result.retryAfterSeconds > 0 ? result.retryAfterSeconds : 60);
                if (!result.Ok || result.status == 204 || token.IsCancellationRequested) { runner.LoadError = SecureTransport.Describe(result); return null; }
                runner.LoadError = "Invalid or unavailable creative. Check the creative URL, format, and dimensions.";
                var response = JsonUtility.FromJson<AdResponse>(Encoding.UTF8.GetString(result.body));
                runner.RememberNoFill(placement, response);
                if (response != null && response.isNoFillOpportunity) {
                    runner.LoadError = "No public ad is available. Your normal ad fallback remains available.";
                    return null;
                }
                if (response == null || string.IsNullOrEmpty(response.requestId) || response.creative == null ||
                    !AdTransport.StoreUrl(response.destinationUrl) || !AdTransport.AllowedUrl(response.creative.url, http) ||
                    response.creative.type < 0 || response.creative.type > 1 ||
                    (placement.format == IndieAdFormat.Banner && response.creative.type != 0) ||
                    (placement.format == IndieAdFormat.Rewarded && response.creative.type != 1)) return null;
                if (response.isPreview != settings.testMode || (settings.testMode ? !string.IsNullOrEmpty(response.ticket) : string.IsNullOrEmpty(response.ticket)) || !DateTimeOffset.TryParse(response.ticketExpires,
                    System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var ticketExpiry)) return null;
                var seconds = Math.Min(settings.cacheMinutes * 60, (ticketExpiry - DateTimeOffset.UtcNow).TotalSeconds - 45);
                if (seconds <= 0) return null;
                ad = new PreparedAd { response = response, test = response.isPreview, expires = Time.realtimeSinceStartupAsDouble + seconds };
                var video = response.creative.type == 1;
                var media = await AdTransport.Send(response.creative.url, null, video ? 30 * 1024 * 1024 : 5 * 1024 * 1024,
                    settings.networkTimeoutSeconds, token);
                if (!media.Ok || media.body == null || token.IsCancellationRequested) { ad.Dispose(); return null; }
                if (!video)
                {
                    // The upload API normalizes all images to PNG. Check dimensions before allocating texture memory.
                    if (!ValidPng(media.body)) { ad.Dispose(); return null; }
                    var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
                    ad.texture = texture;
                    if (!ImageConversion.LoadImage(texture, media.body, true)) { ad.Dispose(); return null; }
                    if (placement.format == IndieAdFormat.Banner && !BannerRatio(placement.bannerSize, texture.width, texture.height))
                    { ad.Dispose(); return null; }
                }
                else
                {
                    ad.file = Path.Combine(folder, requestId + ".mp4");
                    File.WriteAllBytes(ad.file, media.body);
                    var go = new GameObject("Prepared exchange video");
                    go.transform.SetParent(runner.transform);
                    var player = go.AddComponent<VideoPlayer>();
                    ad.video = player;
                    player.playOnAwake = false;
                    player.source = VideoSource.Url;
                    player.url = new Uri(ad.file).AbsoluteUri;
                    player.renderMode = VideoRenderMode.RenderTexture;
                    // Game time/audio are paused during full-screen display. Only the ad
                    // uses unscaled time and an audio source exempt from listener pause.
                    player.timeUpdateMode = VideoTimeUpdateMode.UnscaledGameTime;
                    ad.audio = go.AddComponent<AudioSource>();
                    ad.audio.playOnAwake = false;
                    ad.audio.spatialBlend = 0;
                    ad.audio.ignoreListenerPause = true;
                    ad.audio.mute = true;
                    player.audioOutputMode = VideoAudioOutputMode.AudioSource;
                    player.controlledAudioTrackCount = 1;
                    player.SetTargetAudioSource(0, ad.audio);
                    player.waitForFirstFrame = true;
                    player.isLooping = false;
                    player.errorReceived += (_, __) => ad.failed = true;
                    player.Prepare();
                    var until = Time.realtimeSinceStartupAsDouble + settings.networkTimeoutSeconds;
                    while (!player.isPrepared && !ad.failed && !token.IsCancellationRequested && Time.realtimeSinceStartupAsDouble < until) await Task.Yield();
                    if (!player.isPrepared || ad.failed || token.IsCancellationRequested || player.width == 0 || player.height == 0 ||
                        player.width > 4096 || player.height > 4096 || (long)player.width * player.height > 9000000 ||
                        player.length < 6 || player.length > 30.5) { ad.Dispose(); return null; }
                    var render = new RenderTexture((int)player.width, (int)player.height, 0, RenderTextureFormat.ARGB32);
                    render.Create(); ad.texture = render; player.targetTexture = render;
                    var firstFrame = false;
                    player.sendFrameReadyEvents = true;
                    VideoPlayer.FrameReadyEventHandler frame = (_, __) => firstFrame = true;
                    player.frameReady += frame;
                    player.Play();
                    until = Time.realtimeSinceStartupAsDouble + settings.networkTimeoutSeconds;
                    while (!firstFrame && !ad.failed && !token.IsCancellationRequested && Time.realtimeSinceStartupAsDouble < until) await Task.Yield();
                    player.Pause(); player.frameReady -= frame; player.sendFrameReadyEvents = false;
                    if (!firstFrame || ad.failed || token.IsCancellationRequested) { ad.Dispose(); return null; }
                    if (placement.format == IndieAdFormat.Rewarded) {
                        var rewound = false;
                        VideoPlayer.EventHandler seek = _ => rewound = true;
                        player.seekCompleted += seek; player.time = 0;
                        until = Time.realtimeSinceStartupAsDouble + settings.networkTimeoutSeconds;
                        while (!rewound && !ad.failed && !token.IsCancellationRequested && Time.realtimeSinceStartupAsDouble < until) await Task.Yield();
                        player.seekCompleted -= seek;
                        if (!rewound || ad.failed || token.IsCancellationRequested) { ad.Dispose(); return null; }
                    }
                }
                runner.LoadError = "";
                return ad;
            }
            catch { ad?.Dispose(); return null; }
        }

        internal static bool ValidPng(byte[] data)
        {
            if (data == null || data.Length < 24 || data[0] != 137 || data[1] != 80 || data[2] != 78 || data[3] != 71 ||
                data[4] != 13 || data[5] != 10 || data[6] != 26 || data[7] != 10) return false;
            long w = ((long)data[16] << 24) | ((long)data[17] << 16) | ((long)data[18] << 8) | data[19];
            long h = ((long)data[20] << 24) | ((long)data[21] << 16) | ((long)data[22] << 8) | data[23];
            return w > 0 && h > 0 && w <= 4096 && h <= 4096 && w * h <= 9000000;
        }

        internal static bool BannerRatio(string size, int width, int height)
        {
            var parts = (size ?? "").Split('x');
            return parts.Length == 2 && int.TryParse(parts[0], out var w) && int.TryParse(parts[1], out var h) &&
                (size == "320x50" || size == "320x100" || size == "300x250" || size == "728x90") &&
                width >= w && height >= h && Math.Abs((double)width / height - (double)w / h) < .025;
        }
    }
}
