using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

namespace IndieExchange
{
    internal sealed class PlacementState
    {
        public IndiePlacement placement;
        public IndiePlacementStats stats;
        public string key;
        public PreparedAd ready;
        public bool loading;
        public int failures;
        public double nextLoad;
        public bool dirty;
        public bool requested;
    }

    internal sealed class IndieAdsRunner : MonoBehaviour
    {
        private readonly Dictionary<string, PlacementState> placements = new Dictionary<string, PlacementState>();
        private readonly CancellationTokenSource lifetime = new CancellationTokenSource();
        private IndieAdsSettings settings;
        private TrackingQueue tracking;
        private string folder, installId, scope;
        private bool configurationReady;
        private DateTimeOffset configurationExpires = DateTimeOffset.MaxValue;
        internal bool ConfigurationReady => configurationReady && DateTimeOffset.UtcNow < configurationExpires && !stopped;
        internal static readonly Dictionary<string, LaunchConfiguration> Launches = new Dictionary<string, LaunchConfiguration>();
        private bool http, stopped, paused;
        private AdView fullView, bannerView;
        private PreparedAd fullAd, bannerAd;
        private string bannerPlacement;
        private TaskCompletionSource<bool> showing;
        private bool didOpen;
        private bool reporting;
        private double nextReport;
        private int loadsInFlight;
        public bool IsShowing => showing != null;
        #if UNITY_EDITOR
        internal bool TestForeground;
#endif
        private bool Foreground {
            get {
#if UNITY_EDITOR
                if (TestForeground) return true;
#endif
                return Application.isFocused || settings != null && settings.testMode && Application.isBatchMode;
            }
        }

        internal bool Configure(IndieAdsSettings source)
        {
            settings = source.Snapshot();
            var development = Application.isEditor || Debug.isDebugBuild;
            http = development && settings.allowDevelopmentHttp;
            if (settings.testMode && !development)
            { Debug.LogWarning("Indie Ads: test mode is disabled in release builds. Turn off Test Mode before release."); return false; }
            if (!settings.testMode && (!AdTransport.AllowedUrl(settings.apiBaseUrl, http) || !Guid.TryParse(settings.gameId, out _)))
            { Debug.LogWarning("Indie Ads: enter the Game ID from My games."); return false; }
            if (!Application.isEditor && Application.platform != RuntimePlatform.Android && Application.platform != RuntimePlatform.IPhonePlayer && !settings.testMode)
            { Debug.LogWarning("Indie Ads: live delivery supports Android and iOS."); return false; }
            scope = Hash(settings.apiBaseUrl + "|" + (settings.gameId.Length > 0 ? settings.gameId : settings.sdkKey) + "|" + settings.testMode);
            folder = Path.Combine(Application.persistentDataPath, "IndieExchange", scope);
            try
            {
                Directory.CreateDirectory(folder);
                // Only delete SDK-owned video files in this exact scope directory.
                foreach (var path in Directory.GetFiles(folder, "*.mp4", SearchOption.TopDirectoryOnly)) File.Delete(path);
            }
            catch { Debug.LogWarning("Indie Ads: cache folder unavailable."); return false; }
            var identityKey = "IndieExchange." + scope + ".installation";
            installId = PlayerPrefs.GetString(identityKey, "");
            if (installId.Length == 0) { installId = Guid.NewGuid().ToString("N"); PlayerPrefs.SetString(identityKey, installId); PlayerPrefs.Save(); }
            tracking = new TrackingQueue(folder, settings, lifetime.Token, installId);
            if (settings.testMode) {
                var testPlacements = new[] {
                    new IndiePlacement { id = "interstitial", percentage = 100 },
                    new IndiePlacement { id = "banner", format = IndieAdFormat.Banner, percentage = 100 }
                };
#if UNITY_EDITOR
                if (settings.useLocalTestPlacements) testPlacements = settings.placements;
#endif
                if (!InstallPlacements(testPlacements)) return false;
                configurationReady = true;
            } else _ = DownloadConfiguration();
            return true;
        }

        private bool InstallPlacements(IndiePlacement[] values)
        {
            if (values == null || values.Length > 32) return false;
            foreach (var placement in values)
            {
                if (placement == null || string.IsNullOrWhiteSpace(placement.id) || placement.id.Length > 128 ||
                    placements.ContainsKey(placement.id) || !Enum.IsDefined(typeof(IndieAdFormat), placement.format)) return false;
                if (placement.format == IndieAdFormat.Banner && !ValidBanner(placement.bannerSize)) return false;
                placement.percentage = Mathf.Clamp(placement.percentage, 0, 100);
                var key = "IndieExchange." + scope + "." + Hash(placement.id + "|" + placement.format);
                IndiePlacementStats stats = null;
                try { if (PlayerPrefs.HasKey(key)) stats = JsonUtility.FromJson<IndiePlacementStats>(PlayerPrefs.GetString(key)); } catch { }
                if (stats == null) stats = new IndiePlacementStats {
                    remainder = (int)(BitConverter.ToUInt32(Guid.NewGuid().ToByteArray(),0) % 100),
                    publicRemainder = (int)(BitConverter.ToUInt32(Guid.NewGuid().ToByteArray(),0) % 100)
                };
                stats.remainder = Mathf.Clamp(stats.remainder, 0, 99);
                stats.publicRemainder = Mathf.Clamp(stats.publicRemainder, 0, 99);
                var state = new PlacementState { placement = placement, stats = stats, key = key, dirty = true };
                placements.Add(placement.id, state);
                if (placement.policyRoot && placement.percentage > 0 && placement.format != IndieAdFormat.Rewarded) {
                    foreach (var category in new[] { "public", "personal" }) {
                        if (category == "public" && placement.publicPercentage == 0 || category == "personal" && placement.publicPercentage == 100) continue;
                        var slot = new IndiePlacement { id = placement.id + ":" + category, format = placement.format,
                            percentage = placement.percentage, category = category, bannerSize = placement.bannerSize, bannerPosition = placement.bannerPosition };
                        placements.Add(slot.id, new PlacementState { placement = slot, stats = stats, key = key });
                    }
                }
            }
            return true;
        }

        private async Task DownloadConfiguration()
        {
            try {
                LaunchConfiguration config;
                if (!Launches.TryGetValue(scope, out config)) {
                    // Cache failures as well as successes for this process launch. Scene changes,
                    // repeated Initialize, or Shutdown/Initialize must not start another fetch.
                    Launches.Add(scope, null);
                    var result = await SecureTransport.Send(settings, installId, "configuration", "{}", lifetime.Token);
                    if (!result.Ok || result.body == null || stopped) return;
                    config = JsonUtility.FromJson<LaunchConfiguration>(Encoding.UTF8.GetString(result.body));
                    Launches[scope] = config;
                }
                if (config == null || config.session == null || config.session.Length != 64 || config.formats == null || config.formats.Length != 3 ||
                    !DateTimeOffset.TryParse(config.expires, System.Globalization.CultureInfo.InvariantCulture,
                        System.Globalization.DateTimeStyles.AssumeUniversal, out var expires) || expires <= DateTimeOffset.UtcNow) return;
                var values = new List<IndiePlacement>(); var formats = new HashSet<int>();
                foreach (var item in config.formats) {
                    if (item == null || item.format < 0 || item.format > 2 || !formats.Add(item.format) ||
                        item.percentage < 0 || item.percentage > 100 || item.publicPercentage < 0 || item.publicPercentage > 100 ||
                        !ValidBanner(item.bannerSize) || item.bannerPosition != "top" && item.bannerPosition != "bottom" ||
                        item.format == 1 && item.percentage != 0) return;
                    values.Add(new IndiePlacement { id = item.format == 0 ? "interstitial" : item.format == 2 ? "banner" : "rewarded",
                        format = (IndieAdFormat)item.format, percentage = item.percentage, publicPercentage = item.publicPercentage,
                        bannerSize = item.bannerSize, bannerPosition = item.bannerPosition, policyRoot = true });
                }
                if (stopped) return;
                settings.session = config.session;
                configurationExpires = expires;
                settings.maxCachedAds = 4;
                configurationReady = InstallPlacements(values.ToArray());
            } catch { configurationReady = false; }
        }

        private PlacementState DisplaySlot(PlacementState state)
        {
            if (!state.placement.policyRoot) return state;
            var counter = new PercentageCounter(state.stats.publicRemainder);
            var category = counter.Next(state.placement.publicPercentage) ? "public" : "personal";
            state.stats.publicRemainder = counter.Remainder;
            Save(state);
            return placements.TryGetValue(state.placement.id + ":" + category, out var slot) ? slot : null;
        }

        private static string Hash(string value)
        {
            using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(value))).Replace("-", "").Substring(0, 24);
        }
        private static bool ValidBanner(string value) => value == "320x50" || value == "320x100" || value == "300x250" || value == "728x90";

        private void Update()
        {
            if (stopped || paused || !Foreground || !ConfigurationReady) return;
            foreach (var state in placements.Values)
                if (state.requested && !state.loading && showing == null) _ = Load(state);
            foreach (var state in placements.Values)
            {
                if (state.ready != null && !state.ready.Ready) { state.ready.Dispose(); state.ready = null; }
                if (!state.placement.policyRoot && state.placement.percentage > 0 && state.placement.format != IndieAdFormat.Rewarded &&
                    state.ready == null && !state.loading && Time.realtimeSinceStartupAsDouble >= state.nextLoad &&
                    (showing == null) && (bannerView == null || bannerPlacement != state.placement.id))
                    _ = Load(state);
            }
            if (!settings.testMode) _ = tracking.Tick();
            if (!settings.testMode && !reporting && Time.realtimeSinceStartupAsDouble >= nextReport) _ = ReportStatistics();
        }

        private async Task Load(PlacementState state)
        {
            if (!ConfigurationReady || state.placement.policyRoot || state.loading || state.placement.format == IndieAdFormat.Rewarded ||
                state.ready != null && state.ready.Ready || Time.realtimeSinceStartupAsDouble < state.nextLoad) return;
            if (loadsInFlight > 0 || showing != null) return;
            var cached = 0;
            PlacementState oldest = null;
            foreach (var entry in placements.Values)
                if (entry.ready != null)
                {
                    cached++;
                    if (oldest == null || entry.ready.expires < oldest.ready.expires) oldest = entry;
                }
            if (cached >= settings.maxCachedAds)
            {
                if (!state.requested) return;
                oldest.ready.Dispose(); oldest.ready = null;
            }
            state.requested = false;
            loadsInFlight++;
            state.loading = true;
            try
            {
                var ad = settings.testMode ? PreparedAd.Test(state.placement, settings.cacheMinutes) :
                    await PreparedAd.Load(this, state.placement, settings, folder, installId, http, lifetime.Token);
                if (stopped) { ad?.Dispose(); return; }
                state.ready?.Dispose();
                state.ready = ad;
                state.failures = ad != null ? 0 : Math.Min(6, state.failures + 1);
                state.nextLoad = Time.realtimeSinceStartupAsDouble + (ad != null ? 1 : Math.Min(120, 5 * Math.Pow(2, state.failures - 1)));
            }
            catch { state.nextLoad = Time.realtimeSinceStartupAsDouble + 30; }
            finally { state.loading = false; loadsInFlight--; }
        }

        internal bool IsReady(string id)
        {
            if (!ConfigurationReady || id == null || !placements.TryGetValue(id, out var state)) return false;
            if (state.placement.policyRoot) return IsReady(id + ":public") || IsReady(id + ":personal");
            return state.ready != null && state.ready.Ready;
        }
        internal void Preload(string id)
        {
            if (ConfigurationReady && id != null && placements.TryGetValue(id, out var state))
            {
                if (state.placement.policyRoot) { Preload(id + ":public"); Preload(id + ":personal"); return; }
                if (state.ready != null && state.ready.Ready || state.placement.format == IndieAdFormat.Rewarded) return;
                state.requested = true;
                _ = Load(state);
            }
        }
        internal IndiePlacementStats GetStatistics(string id) => id != null && placements.TryGetValue(id, out var s) ? s.stats.Copy() : new IndiePlacementStats();

        private bool Select(PlacementState state, bool bypass)
        {
            if (bypass && settings.testMode) return true; // Explicit no-fill recovery is not a second percentage opportunity.
            state.stats.opportunities++;
            var counter = new PercentageCounter(state.stats.remainder);
            var selected = counter.Next(state.placement.percentage);
            state.stats.remainder = counter.Remainder;
            if (selected) state.stats.selected++;
            Save(state);
            return selected;
        }
        private void Save(PlacementState state)
        {
            state.dirty = true;
            PlayerPrefs.SetString(state.key, JsonUtility.ToJson(state.stats));
            PlayerPrefs.Save();
            var id = state.placement.category == null ? state.placement.id : state.placement.id.Split(':')[0];
            if (placements.TryGetValue(id, out var root)) root.dirty = true;
            IndieAds.Stats(id, state.stats);
        }
        private async Task ReportStatistics()
        {
            reporting = true;
            nextReport = Time.realtimeSinceStartupAsDouble + 600; // Attested diagnostics must not dominate the device's integrity quota.
            try
            {
                foreach (var state in placements.Values)
                {
                    if (state.placement.category != null || !state.dirty || state.stats.opportunities == 0 && state.stats.shown == 0) continue;
                    state.dirty = false;
                    var stats = state.stats.Copy();
                    var body = new StatisticsReport { sdkKey = settings.gameId, installationId = installId,
                        placementId = state.placement.id, format = (int)state.placement.format,
                        opportunities = stats.opportunities, selected = stats.selected, shown = stats.shown,
                        unavailable = stats.unavailable, fallbackShown = stats.fallbackShown };
                    var result = await SecureTransport.Send(settings, installId, "placement-stats", JsonUtility.ToJson(body), lifetime.Token);
                    if (!result.Ok) state.dirty = true;
                    if (stopped) return;
                    break; // One small snapshot per interval; never block rendering.
                }
            }
            finally { reporting = false; }
        }
        private bool Eligible(string id, IndieAdFormat format, out PlacementState state)
        {
            state = null;
            return ConfigurationReady && !paused && Foreground && id != null &&
                placements.TryGetValue(id, out state) && state.placement.format == format && state.placement.category == null;
        }

        internal Task<bool> TryShow(string id, bool bypass)
        {
            // Coalesce a second call while a full-screen ad is active. Never launch the host fallback over it.
            if (showing != null) return showing.Task;
            if (!Eligible(id, IndieAdFormat.Interstitial, out var state) || !Select(state, bypass)) return Task.FromResult(false);
            var root = state;
            state = DisplaySlot(root);
            if (state == null || !IsReady(state.placement.id))
            {
                root.stats.unavailable++; Save(root);
                return Task.FromResult(false);
            }
            HideBanner();
            var ad = state.ready; state.ready = null; fullAd = ad;
            showing = new TaskCompletionSource<bool>();
            var result = showing.Task;
            didOpen = false;
            try
            {
                fullView = AdView.Create(transform, ad, state.placement,
                    () =>
                    {
                        didOpen = true;
                        RecordShown(state, ad, bypass);
                        IndieAds.Opened(id);
                    },
                    () => Track("click", ad),
                    () => Finish(id));
                // A fully cached creative is attached to the active overlay now.
                // Mark the outcome handled even if closed before next-frame impression reporting.
                didOpen = true;
            }
            catch
            {
                state.stats.unavailable++; Save(state);
                Finish(id);
            }
            return result;
        }

        private void Finish(string id)
        {
            if (showing == null) return;
            var completion = showing; var opened = didOpen;
            fullView = null; fullAd?.Dispose(); fullAd = null;
            showing = null; didOpen = false;
            if (opened) IndieAds.Closed(id);
            completion.TrySetResult(opened);
            if (placements.TryGetValue(id, out var state)) state.nextLoad = Time.realtimeSinceStartupAsDouble + 1;
        }

        internal bool TryBanner(string id, bool bypass)
        {
            if (showing != null) return false;
            if (bannerView != null)
            {
                if (bannerPlacement == id) return true;
                HideBanner();
            }
            if (!Eligible(id, IndieAdFormat.Banner, out var state) || !Select(state, bypass)) return false;
            var root = state;
            state = DisplaySlot(root);
            if (state == null || !IsReady(state.placement.id)) { root.stats.unavailable++; Save(root); return false; }
            var ad = state.ready; state.ready = null;
            bannerAd = ad; bannerPlacement = id;
            try
            {
                bannerView = AdView.Create(transform, ad, state.placement,
                    () => RecordShown(state, ad, bypass), () => Track("click", ad), () =>
                    { bannerView = null; bannerAd?.Dispose(); bannerAd = null; bannerPlacement = null; });
                return true;
            }
            catch { ad.Dispose(); bannerAd = null; bannerPlacement = null; return false; }
        }
        internal void HideBanner()
        {
            if (bannerView != null) bannerView.Close();
            bannerView = null;
        }
        private void RecordShown(PlacementState state, PreparedAd ad, bool bypass)
        {
            state.stats.shown++;
            if (bypass && settings.testMode) state.stats.fallbackShown++;
            Save(state);
            Track("impression", ad);
        }
        private void Track(string route, PreparedAd ad)
        {
            if (!ad.test && !stopped) tracking.Add(route, ad.response.requestId, ad.response.ticket);
        }
        private void OnApplicationPause(bool value) { paused = value; }
        internal void Stop()
        {
            if (stopped) return;
            stopped = true;
            lifetime.Cancel();
            if (fullView != null) fullView.Close();
            if (showing != null) { showing.TrySetResult(didOpen); showing = null; }
            fullAd?.Dispose(); fullAd = null;
            HideBanner();
            foreach (var state in placements.Values) { state.ready?.Dispose(); state.ready = null; }
            if (settings != null) Destroy(settings);
        }
        private void OnDestroy() { Stop(); if (IndieAds.Runner == this) IndieAds.Runner = null; }
    }
}
