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 string file;
        public bool test, failed;
        public double expires;
        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 texture = new Texture2D(320, 180, TextureFormat.RGB24, false);
            var pixels = new Color[320 * 180];
            for (var y = 0; y < 180; y++)
                for (var x = 0; x < 320; x++) pixels[y * 320 + x] = Color.Lerp(new Color(.08f,.15f,.27f), new Color(.18f,.68f,.56f), (x + y) / 500f);
            texture.SetPixels(pixels); texture.Apply();
            return new PreparedAd { texture = texture, test = true, expires = Time.realtimeSinceStartupAsDouble + minutes * 60,
                response = new AdResponse { requestId = Guid.NewGuid().ToString("N"), gameName = "Indie Exchange test ad",
                    headline = "Your next adventure starts here", ctaText = "Test install button",
                    creative = new AdCreative { type = 0 } } };
        }

        public static async Task<PreparedAd> Load(IndieAdsRunner runner, IndiePlacement placement, IndieAdsSettings settings,
            string folder, string installId, bool http, CancellationToken token)
        {
            PreparedAd ad = null;
            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.Ok || result.status == 204 || token.IsCancellationRequested) return null;
                var response = JsonUtility.FromJson<AdResponse>(Encoding.UTF8.GetString(result.body));
                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)) return null;
                if (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 - 20);
                if (seconds <= 0) return null;
                ad = new PreparedAd { response = response, 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;
                    player.audioOutputMode = VideoAudioOutputMode.Direct;
                    player.controlledAudioTrackCount = 1;
                    player.SetDirectAudioMute(0, true);
                    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; }
                }
                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;
        }
    }
}
