using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace Mocapless
{
    // What the Mocapless API answers, shaped for JsonUtility (public fields, no properties).

    [Serializable]
    public class Me
    {
        public string id;
        public string email;
        public int credits;
        public string plan;
    }

    [Serializable]
    public class Character
    {
        public string id;
        public string filename;
        public string status;     // pending, processing, ready, failed
        public string error;
        public string family;     // quadruped, humanoid, bird, frog
        public string stage;
        public bool unlocked;
        public int boneCount;
    }

    [Serializable]
    class CharacterList
    {
        public List<Character> items = new List<Character>();
    }

    [Serializable]
    public class Segment
    {
        public string primitive;
        public float seconds;
    }

    [Serializable]
    public class Script
    {
        public List<Segment> segments = new List<Segment>();
    }

    [Serializable]
    public class Plan
    {
        public Script script;
        public int cost;
        public int balance;
        public bool affordable;
        public List<string> notes = new List<string>();
    }

    [Serializable]
    public class Output
    {
        public string kind;
        public string url;
        public bool locked;
    }

    [Serializable]
    public class Job
    {
        public string id;
        public string status;     // queued, running, succeeded, failed, cancelled
        public string stage;
        public float progress;
        public string prompt;
        public string error;
        public int creditsCost;
        public bool creditsRefunded;
        public float durationSeconds;
        public string assetId;
        public string fallbackFrom;
        public List<Output> outputs = new List<Output>();

        public Output Find(string kind)
        {
            foreach (var o in outputs)
                if (o.kind == kind) return o;
            return null;
        }
    }

    [Serializable]
    class PlanRequest
    {
        public string prompt;
        public string assetId;
    }

    [Serializable]
    class GenerateRequest
    {
        public string prompt;
        public string assetId;
        public string[] formats = { "fbx" };
    }

    [Serializable]
    class ErrorText
    {
        public string detail;
    }

    public class MocaplessException : Exception
    {
        public readonly int Status;
        public MocaplessException(int status, string message) : base(message) { Status = status; }
    }

    /// <summary>The few calls the window needs, against https://api.mocapless.com with an API key.</summary>
    public static class MocaplessApi
    {
        public const string Version = "0.1.0";
        public const string Site = "https://mocapless.com";
        const string DefaultBase = "https://api.mocapless.com";

        static HttpClient _client;

        static HttpClient Client
        {
            get
            {
                if (_client == null)
                {
                    _client = new HttpClient { Timeout = TimeSpan.FromMinutes(15) };
                    _client.DefaultRequestHeaders.UserAgent.ParseAdd($"mocapless-unity/{Version}");
                }
                return _client;
            }
        }

        public static string BaseUrl => (Environment.GetEnvironmentVariable("MOCAPLESS_API") ?? DefaultBase).TrimEnd('/');

        static HttpRequestMessage Request(HttpMethod method, string path, string key)
        {
            var req = new HttpRequestMessage(method, BaseUrl + path);
            req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
            return req;
        }

        static async Task<string> Send(HttpRequestMessage req)
        {
            using (req)
            using (var res = await Client.SendAsync(req))
            {
                var body = await res.Content.ReadAsStringAsync();
                if (!res.IsSuccessStatusCode)
                    throw new MocaplessException((int)res.StatusCode, Explain((int)res.StatusCode, body));
                return body;
            }
        }

        /// <summary>The API's own words when it gives them ("detail"), else something a person can act on.</summary>
        static string Explain(int status, string body)
        {
            string detail = null;
            try { detail = JsonUtility.FromJson<ErrorText>(body)?.detail; } catch { /* a list of validation errors */ }
            if (!string.IsNullOrEmpty(detail)) return detail;
            switch (status)
            {
                case 401: return "That API key was not accepted. Make a new one at mocapless.com/account.";
                case 402: return "Not enough credits for that. Top up at mocapless.com/credits.";
                case 413: return "That file is too large to upload.";
                case 429: return "Too many requests for a moment. Try again in a minute.";
                default: return $"Mocapless answered {status}. Try again, or write to us if it keeps happening.";
            }
        }

        static StringContent Json(object payload) =>
            new StringContent(JsonUtility.ToJson(payload), Encoding.UTF8, "application/json");

        public static async Task<Me> GetMe(string key) =>
            JsonUtility.FromJson<Me>(await Send(Request(HttpMethod.Get, "/v1/auth/me", key)));

        public static async Task<List<Character>> ListCharacters(string key)
        {
            var body = await Send(Request(HttpMethod.Get, "/v1/assets", key));
            return JsonUtility.FromJson<CharacterList>("{\"items\":" + body + "}").items;
        }

        public static async Task<Character> GetCharacter(string key, string id) =>
            JsonUtility.FromJson<Character>(await Send(Request(HttpMethod.Get, $"/v1/assets/{id}", key)));

        public static async Task<Character> Upload(string key, string path)
        {
            var req = Request(HttpMethod.Post, "/v1/assets", key);
            var form = new MultipartFormDataContent();
            var file = new StreamContent(File.OpenRead(path));
            file.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
            form.Add(file, "file", Path.GetFileName(path));
            req.Content = form;
            return JsonUtility.FromJson<Character>(await Send(req));
        }

        public static async Task<Plan> MakePlan(string key, string prompt, string assetId)
        {
            var req = Request(HttpMethod.Post, "/v1/plan", key);
            req.Content = Json(new PlanRequest { prompt = prompt, assetId = assetId });
            return JsonUtility.FromJson<Plan>(await Send(req));
        }

        public static async Task<Job> Generate(string key, string prompt, string assetId)
        {
            var req = Request(HttpMethod.Post, "/v1/generate", key);
            req.Content = Json(new GenerateRequest { prompt = prompt, assetId = assetId });
            return JsonUtility.FromJson<Job>(await Send(req));
        }

        public static async Task<Job> GetJob(string key, string id) =>
            JsonUtility.FromJson<Job>(await Send(Request(HttpMethod.Get, $"/v1/jobs/{id}", key)));

        /// <summary>Downloads are signed links: no key goes with them.</summary>
        public static async Task Download(string url, string path)
        {
            using (var res = await Client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
            {
                if (!res.IsSuccessStatusCode)
                    throw new MocaplessException((int)res.StatusCode, "The download link did not work. Press Check again.");
                Directory.CreateDirectory(Path.GetDirectoryName(path));
                var tmp = path + ".part";
                using (var src = await res.Content.ReadAsStreamAsync())
                using (var dst = File.Create(tmp))
                    await src.CopyToAsync(dst);
                if (File.Exists(path)) File.Delete(path);
                File.Move(tmp, path);
            }
        }
    }
}
