/*
    TYTD Lite: A YouTube Downloader website that archives videos when people download videos using it.
    Copyright (C) 2024 Mike Nolan
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Humanizer;
using LiteDB;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Scriban;
using Tesses.WebServer;
using YoutubeExplode;
using YoutubeExplode.Exceptions;
using YoutubeExplode.Videos;
using YoutubeExplode.Videos.Streams;

namespace TYTDLite
{
    public class TYTDLiteServer
    {
        public ILiteDatabase Database {get;}
        public ILiteCollection<SavedVideo> Videos => Database.GetCollection<SavedVideo>("Videos");

         List<Job> Jobs {get;set;}=new List<Job>();
        public string ServerPath {get;}

        public RouteServer Server {get;}

        public YoutubeClient YoutubeClient {get;}
        public HttpClient HttpClient {get;}
        
        Lazy<Template> template;
        Lazy<Template> pageIndex;
        Lazy<Template> pageDownload;
        private Lazy<Template> pageInitDownload;
        SemaphoreSlim semaphore=new SemaphoreSlim(1,1);

        bool HasConverter => File.Exists(Path.Combine(ServerPath,"converter.txt"));
        string ConverterPath => File.ReadAllText(Path.Combine(ServerPath,"converter.txt")).Replace("\r","").Replace("\n","");

        public TYTDLiteServer(string serverPath,HttpClient client)
        {
            template = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/template.html")),true);
            pageIndex = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/index.html")),true);
            pageDownload = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/video_download.html")),true);
            pageInitDownload = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/video_init_download.html")),true);
            
            ServerPath = serverPath;
            Directory.CreateDirectory(ServerPath);
            Database = new LiteDatabase(Path.Combine(ServerPath,"tytdlite.db"));
            
            Server = new RouteServer(new AssetProvider());
            HttpClient = client;
            YoutubeClient = new YoutubeClient(client);
            Server.Add("/",IndexAsync,"GET");
            Server.Add("/thumbnail",ThumbnailAsync,"GET");
            Server.Add("/download",DownloadAsync,"GET");
            Server.Add("/video",VideoAsync,"GET");
            Server.Add("/init_download",InitDownloadAsync,"GET");
            Server.Add("/init_download_event",InitDownloadEventAsync,"GET");
            Server.Add("/info.json",InfoAsync,"GET");
            Server.Add("/count",CountAsync,"GET");
        }

        public static void MainFunc(string[] args)
        {
            
int port = 4220;
string path = "data";
HttpClient client=null;
if(args.Length >= 1)
{
    if(args[0] == "--help")
    {
        Console.WriteLine("TYTD Lite: A YouTube Downloader website that archives videos when people download videos using it.");
        Console.WriteLine("Created by Tesses (Mike Nolan): https://tesses.net/");
        Console.WriteLine("Released under the GPLv3 License, for more info go: https://www.gnu.org/licenses/");
        Console.WriteLine("Thanks for using this software, consider buying me a coffee: https://www.buymeacoffee.com/tesses50");
        Console.WriteLine();
        Console.WriteLine("USAGE:");
        Console.WriteLine("tytdlite");
        Console.WriteLine("tytdlite <path>");
        Console.WriteLine("tytdlite <path> <port>");
        Console.WriteLine("tytdlite <path> <port> <ignorecert:true|false>");
        return;
    }
    else
    {
        path = args[0];
        if(args.Length >= 2)
        {
            if(int.TryParse(args[1],out var p)) port = p;

            if(args.Length >= 3)
            {
                if(args[2] == "true")
                {
                    System.Net.ServicePointManager.ServerCertificateValidationCallback += (a,b,c,d)=>true;
                    HttpClientHandler ch=new HttpClientHandler();
                    ch.ServerCertificateCustomValidationCallback = (a,b,c,d)=>true;
                    client = new HttpClient(ch);
                }
            }
        }
    }
}

TYTDLiteServer server=new TYTDLiteServer(path,client ?? new HttpClient());
server.Server.StartServer(port);
        }

        private async Task CountAsync(ServerContext ctx)
        {   
            await ctx.SendJsonAsync(new{count = Videos.LongCount()});
        }

        private async Task InfoAsync(ServerContext ctx)
        {
            if(ctx.QueryParams.TryGetFirst("v",out var v))
            {
                var video=VideoId.TryParse(v);
                if(video.HasValue)
                {
                    var (_v,rfy,off)=await GetSavedVideoAsync(video.Value);
                    await ctx.SendJsonAsync(new{Video=_v,RemovedFromYouTube=rfy,Offline=off});
                }
                else
                    await ctx.SendNotFoundAsync();
            }else{
                await ctx.SendNotFoundAsync();
            }
        }

        private async Task InitDownloadAsync(ServerContext ctx)
        {
            
            if(ctx.QueryParams.TryGetFirst("v",out var v) && ctx.QueryParams.TryGetFirst("res",out var res))
            {
                var video = VideoId.TryParse(v);
            
                if(video.HasValue && reses.Contains(res))
                {
                    
                    await ctx.SendTextAsync(await template.Value.RenderAsync(new{body=await pageInitDownload.Value.RenderAsync(new{v,res})}));
                }
            }
        }   
        string[] reses=new string[]{"PreMuxed","VideoOnly","AudioOnly","MKV","MP3","MP4"};
        private async Task InitDownloadEventAsync(ServerContext ctx)
        {
            if(ctx.QueryParams.TryGetFirst("v",out var v) && ctx.QueryParams.TryGetFirst("res",out var res))
            {
                var video = VideoId.TryParse(v);
            
                if(video.HasValue && reses.Contains(res))
                {
                    SendEvents evts=new SendEvents();

                    Task.Run(()=>ctx.ServerSentEvents(evts)).Wait(0);
                    try{
                     
                        await this.PreformTask(video.Value,res,new Progress<double>(e=>{
                            try{
                            evts.SendEvent(new{type="progress",progress=e});
                            }catch(Exception){}
                        }));
                        evts.SendEvent(new{type="done",url=$"./video?v={video.Value.Value}&res={res}"});
                        

                    }catch(Exception ex)
                    {
                        try{
                        evts.SendEvent(new{type="error",error=ex});
                        
                        }catch(Exception){}
                    }
                   
                    
                }
                else
                {
                    await ctx.SendExceptionAsync(new ArgumentException("Must provide video id \"v\" and \"res\" must be PreMuxed, VideoOnly, AudioOnly, MKV, MP3 or MP4"));
                }
            }
            else
            {
                await ctx.SendExceptionAsync(new ArgumentException("Must provide video id \"v\" and \"res\" must be PreMuxed, VideoOnly, AudioOnly, MKV, MP3 or MP4"));
            }
        }

        private async Task VideoAsync(ServerContext ctx)
        {
            if(ctx.QueryParams.TryGetFirst("v",out var v) && ctx.QueryParams.TryGetFirst("res",out var res))
            {
                var video = VideoId.TryParse(v);
            
                if(video.HasValue && reses.Contains(res))
                {
                    await PreformTask(video.Value,res,new Progress<double>(e=>{
                          
                    }));
                    var (svideo,_,_) = await GetSavedVideoAsync(video.Value);

                    switch(res)
                    {
                        case "PreMuxed":
                        {
                            string ext = svideo.PreMuxed != null ? svideo.PreMuxed.Container : "mp4";
                            await ctx.WithFileName(svideo.SafeFileName(ext),false).SendFileAsync(Path.Combine(ServerPath,"PreMuxed",$"{svideo.VideoId}.{ext}"));
                        }
                        break;
                        case "VideoOnly":
                        {
                            string ext = svideo.VideoOnly != null ? svideo.VideoOnly.Container : "mp4";
                            await ctx.WithFileName(svideo.SafeFileName(ext),false).SendFileAsync(Path.Combine(ServerPath,"VideoOnly",$"{svideo.VideoId}.{ext}"));
                        }
                        break;
                        case "AudioOnly":
                        {
                            string ext = svideo.AudioOnly != null ? svideo.AudioOnly.Container : "mp4";
                            await ctx.WithFileName(svideo.SafeFileName(ext),false).SendFileAsync(Path.Combine(ServerPath,"AudioOnly",$"{svideo.VideoId}.{ext}"));
                        }
                        break;
                        case "MP3":
                        {
                            await ctx.WithFileName(svideo.SafeFileName("mp3"),false).SendFileAsync(Path.Combine(ServerPath,"MP3",$"{svideo.VideoId}.mp3"));
                        }
                        break;
                        case "MP4":
                        {
                            await ctx.WithFileName(svideo.SafeFileName("mp4"),false).SendFileAsync(Path.Combine(ServerPath,"MP4",$"{svideo.VideoId}.mp4"));
                        }
                        break;
                        case "MKV":
                        {
                            await ctx.WithFileName(svideo.SafeFileName("mkv"),false).SendFileAsync(Path.Combine(ServerPath,"MKV",$"{svideo.VideoId}.mkv"));
                        }
                        break;
                    }

                }
            }
        }

        private async Task<(SavedVideo video,bool removedByYt,bool offline)> GetSavedVideoAsync(VideoId id)
        {
            string _id = id.Value;
            bool removedByYt=false;
            bool offline=false;
            SavedVideo video=new SavedVideo();
            await LockAsync(async()=>{
                var _video=Videos.FindOne(e=>e.VideoId == _id);
                if(_video != null)
                {
                    video = _video;
                    DateTime now = DateTime.Now;
                    if(video.Expires < now)
                    {
                        try{
                            var streams = await YoutubeClient.Videos.Streams.GetManifestAsync(id);
                            video.PreMuxed.Stream = streams.GetMuxedStreams().GetWithHighestVideoQuality();
                            video.VideoOnly.Stream = streams.GetVideoOnlyStreams().GetWithHighestVideoQuality();
                            video.AudioOnly.Stream = streams.GetAudioOnlyStreams().GetWithHighestBitrate();
                            video.Expires = now.AddHours(6);
                            Videos.Update(video);
                        }
                        catch(VideoRequiresPurchaseException ex)
                        {
                            _=ex;
                            removedByYt=true;
                        }
                        catch(VideoUnavailableException ex)
                        {
                            _=ex;
                            removedByYt=true;
                        }
                        catch(VideoUnplayableException ex)
                        {
                            _=ex;
                            removedByYt=true;
                        }
                        catch(Exception ex)
                        {
                            _=ex;
                            offline=true;
                        }
                    }
                }
                else
                {
                    try{
                        var now = DateTime.Now;
                        video = await YoutubeClient.Videos.GetAsync(id);
                        var streams = await YoutubeClient.Videos.Streams.GetManifestAsync(id);
                        video.PreMuxed.Stream = streams.GetMuxedStreams().GetWithHighestVideoQuality();
                        video.VideoOnly.Stream = streams.GetVideoOnlyStreams().GetWithHighestVideoQuality();
                        video.AudioOnly.Stream = streams.GetAudioOnlyStreams().GetWithHighestBitrate();
                        video.Expires = now.AddHours(6);
                        video.Id=Videos.Insert(video);
                        await DownloadThumbnailsAsync(id);
                    }catch(VideoRequiresPurchaseException ex)
                        {
                            _=ex;
                            removedByYt=true;
                        }
                        catch(VideoUnavailableException ex)
                        {
                            _=ex;
                            removedByYt=true;
                        }
                        catch(VideoUnplayableException ex)
                        {
                            _=ex;
                            removedByYt=true;
                        }
                }
            });
            return (video,removedByYt,offline);
        }

        private async Task DownloadThumbnailsAsync(VideoId id)
        {
            async Task DLAsync(string resolution)
            {
                try{
                string dir = Path.Combine(ServerPath,"Thumbnails",id.Value);
                string filename = Path.Combine(dir,$"{resolution}.jpg");

                Directory.CreateDirectory(dir);
                if(!File.Exists(filename))
                {
                    File.WriteAllBytes(filename,await HttpClient.GetByteArrayAsync($"https://s.ytimg.com/vi/{id.Value}/{resolution}.jpg"));
                }
                }catch(Exception ex){_=ex;}
            }
            await DLAsync("default");
            await DLAsync("sddefault");
            await DLAsync("mqdefault");
            await DLAsync("hqdefault");
            await DLAsync("maxresdefault");
        }

        private async Task ConvertAsync(string command,IProgress<TimeSpan> progress)
        {
            if(!HasConverter) return;
            using(Process p=new Process())
            {
                p.StartInfo.CreateNoWindow = true;
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                p.StartInfo.FileName = ConverterPath;
                p.StartInfo.Arguments = command;
                p.StartInfo.RedirectStandardError=true;
                p.Start();
                while(!p.HasExited)
                {
                    
                    var res=await p.StandardError.ReadLineAsync();
                    if(!string.IsNullOrEmpty(res))
                    {
                        int index = res.IndexOf("time="); 
                        if(index > -1)
                        {
                            index+=5;
                            int index2 = res.IndexOf(' ',index);
                            string timeStr = index2 == -1 ? res.Substring(index) : res.Substring(index,index2-index);
                            if(!string.IsNullOrWhiteSpace(timeStr))
                            {
                                if(TimeSpan.TryParse(timeStr,out var ts))
                                {
                                    progress?.Report(ts);
                                }
                                else
                                {
                                    if(double.TryParse(timeStr,out var tsn)) //for avconv
                                    {
                                        progress?.Report(TimeSpan.FromSeconds(tsn));
                                    }
                                }
                            }
                        }                      
                    }
                }
            }
        }
        
        private async Task DownloadPreMuxed(SavedVideo video,IProgress<double> progress=null)
        {
            
            string dir = Path.Combine(ServerPath,"PreMuxed");
            string complete=Path.Combine(dir,$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
            string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
            if(Directory.Exists(dir) && File.Exists(complete)) return;
            //assume saved video has not expired
            if(!Directory.Exists(dir))
            Directory.CreateDirectory(dir);
            var killSwitch = video.Expires-DateTime.Now;
            using(CancellationTokenSource src=new CancellationTokenSource())
            {
                src.CancelAfter(killSwitch);
                var token = src.Token;
                byte[] buffer=new byte[4096];
                using(var destStrm = File.Open(incomplete,FileMode.OpenOrCreate,FileAccess.Write))
                {
                    destStrm.Position = destStrm.Length;
                    using(var srcStrm = await YoutubeClient.Videos.Streams.GetAsync(video.PreMuxed.Stream,token))
                    {
                        long pos = destStrm.Position;
                        long len = srcStrm.Length;
                        if(pos > 0)
                        srcStrm.Position = pos;
                        int read=0;
                        do {
                            read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
                            if(token.IsCancellationRequested) return;
                            await destStrm.WriteAsync(buffer,0,read,token);
                            if(token.IsCancellationRequested) return;
                            pos+=read;
                            progress?.Report(pos / (double)len);
                        } while(read != 0 && !token.IsCancellationRequested);
                        if(token.IsCancellationRequested) return;
                        File.Move(incomplete,complete);
                    }
                }
            }
        }
        private async Task DownloadVideoOnly(SavedVideo video,IProgress<double> progress=null)
        {
            string dir = Path.Combine(ServerPath,"VideoOnly");
            string complete=Path.Combine(dir,$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
            string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
            if(Directory.Exists(dir) && File.Exists(complete)) return;
            //assume saved video has not expired
            if(!Directory.Exists(dir))
            Directory.CreateDirectory(dir);
            var killSwitch = video.Expires - DateTime.Now;
            using(CancellationTokenSource src=new CancellationTokenSource())
            {
                src.CancelAfter(killSwitch);
                var token = src.Token;
                byte[] buffer=new byte[4096];
                using(var destStrm = File.Open(incomplete,FileMode.OpenOrCreate,FileAccess.Write))
                {
                    destStrm.Position = destStrm.Length;
                    using(var srcStrm = await YoutubeClient.Videos.Streams.GetAsync(video.VideoOnly.Stream,token))
                    {
                        long pos = destStrm.Position;
                        long len = srcStrm.Length;
                        if(pos > 0)
                        srcStrm.Position = pos;
                        int read=0;
                        do {
                            read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
                            if(token.IsCancellationRequested) return;
                            await destStrm.WriteAsync(buffer,0,read,token);
                            if(token.IsCancellationRequested) return;
                            pos+=read;
                            progress?.Report(pos / (double)len);
                        } while(read != 0 && !token.IsCancellationRequested);
                        if(token.IsCancellationRequested) return;
                        File.Move(incomplete,complete);
                    }
                }
            }
        }
       
        private async Task DownloadAudioOnly(SavedVideo video,IProgress<double> progress=null)
        {
            string dir = Path.Combine(ServerPath,"AudioOnly");
            string complete=Path.Combine(dir,$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
            string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
            if(Directory.Exists(dir) && File.Exists(complete)) return;
            //assume saved video has not expired
            if(!Directory.Exists(dir))
            Directory.CreateDirectory(dir);
            var killSwitch = video.Expires - DateTime.Now;
            using(CancellationTokenSource src=new CancellationTokenSource())
            {
                src.CancelAfter(killSwitch);
                var token = src.Token;
                byte[] buffer=new byte[4096];
                using(var destStrm = File.Open(incomplete,FileMode.OpenOrCreate,FileAccess.Write))
                {
                    destStrm.Position = destStrm.Length;
                    using(var srcStrm = await YoutubeClient.Videos.Streams.GetAsync(video.AudioOnly.Stream,token))
                    {
                        long pos = destStrm.Position;
                        long len = srcStrm.Length;
                        if(pos > 0)
                        srcStrm.Position = pos;
                        int read=0;
                        do {
                            read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
                            if(token.IsCancellationRequested) return;
                            await destStrm.WriteAsync(buffer,0,read,token);
                            if(token.IsCancellationRequested) return;
                            pos+=read;
                            progress?.Report(pos / (double)len);
                        } while(read != 0 && !token.IsCancellationRequested);
                        if(token.IsCancellationRequested) return;
                        File.Move(incomplete,complete);
                    }
                }
            }
        }
       
        private async Task ConvertToMp3(SavedVideo video,IProgress<double> progress=null)
        {
            string dir = Path.Combine(ServerPath,"MP3");
            string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.mp3");
            string complete = Path.Combine(dir,$"{video.VideoId}.mp3");
            string audioonly=Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
            
            string premuxed = Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
            if(!Directory.Exists(dir))
                Directory.CreateDirectory(dir);
            if(File.Exists(complete)) return;
            await PreformTask(video.VideoId,"AudioOnly",new Progress<double>(e=>{
                progress?.Report(e / 2);
            }));
            if(!File.Exists(audioonly))
            {
                await PreformTask(video.VideoId,"PreMuxed",new Progress<double>(e=>{
                    progress?.Report(e/2);
                }));
                audioonly = premuxed;
            }
            if(File.Exists(audioonly) && !File.Exists(complete))
            {
                if(File.Exists(incomplete))
                    File.Delete(incomplete);
                await ConvertAsync($"-i \"{audioonly}\" -c:a libmp3lame -vn -preset ultrafast \"{incomplete}\"",new Progress<TimeSpan>((v)=>{
                 progress?.Report(( v.TotalSeconds/ video.Duration.TotalSeconds/2)+0.5);
                }));
            }
            if(File.Exists(incomplete))
                File.Move(incomplete,complete);
        }
        private async Task MuxToMkv(SavedVideo video,IProgress<double> progress=null)
        {
            string dir = Path.Combine(ServerPath,"MKV");
            string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.mkv");
            string complete = Path.Combine(dir,$"{video.VideoId}.mkv");
            string audioonly=Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
            string videoonly=Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
            string premuxed = Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");

            if(File.Exists(complete)) return;
            if(!Directory.Exists(dir))
                Directory.CreateDirectory(dir);
             await PreformTask(video.VideoId,"VideoOnly",new Progress<double>(e=>{
                progress?.Report(e / 3);
            }));
            await PreformTask(video.VideoId,"AudioOnly",new Progress<double>(e=>{
                progress?.Report((e/3)+0.3333);
            }));
            if(!File.Exists(audioonly))
            {
                await PreformTask(video.VideoId,"PreMuxed",new Progress<double>(e=>{
                    progress?.Report((e/3)+0.3333);
                }));
                audioonly = premuxed;
            }
            if(File.Exists(audioonly) && File.Exists(videoonly) && !File.Exists(complete))
            {
                if(File.Exists(incomplete))
                    File.Delete(incomplete);
                await ConvertAsync($"-i \"{videoonly}\" -i \"{audioonly}\" -c copy -map 0:v -map 1:a -preset ultrafast \"{incomplete}\"",new Progress<TimeSpan>((v)=>{
                    progress?.Report(( v.TotalSeconds/video.Duration.TotalSeconds/3)+0.6666); //no we are not worshiping satan that is 2/3
                }));
            }
            if(File.Exists(incomplete))
                File.Move(incomplete,complete);
        }

         private async Task ConvertToMp4(SavedVideo video,IProgress<double> progress=null)
        {
            string dir = Path.Combine(ServerPath,"MP4");
            string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.mp4");
            string complete = Path.Combine(dir,$"{video.VideoId}.mp4");
            string audioonly=Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
            string videoonly=Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
            string premuxed = Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");

            if(File.Exists(complete)) return;
            if(!Directory.Exists(dir))
                Directory.CreateDirectory(dir);
             await PreformTask(video.VideoId,"VideoOnly",new Progress<double>(e=>{
                progress?.Report(e / 3);
            }));
            await PreformTask(video.VideoId,"AudioOnly",new Progress<double>(e=>{
                progress?.Report((e/3)+33.3333);
            }));
            if(!File.Exists(audioonly))
            {
                await PreformTask(video.VideoId,"PreMuxed",new Progress<double>(e=>{
                    progress?.Report((e/3)+33.3333);
                }));
                audioonly = premuxed;
            }
            if(File.Exists(audioonly) && File.Exists(videoonly) && !File.Exists(complete))
            {
                if(File.Exists(incomplete))
                    File.Delete(incomplete);
                await ConvertAsync($"-i \"{videoonly}\" -i \"{audioonly}\" -c:v libx264 -c:a aac -map 0:v -map 1:a -preset ultrafast \"{incomplete}\"",new Progress<TimeSpan>((v)=>{
                    progress?.Report((v.TotalSeconds/video.Duration.TotalSeconds/3)+66.6666); //no we are not worshiping satan that is 2/3
                }));
            }
            if(File.Exists(incomplete))
                File.Move(incomplete,complete);
        }
        private async Task PreformTask(VideoId id, string resolution,IProgress<double> progress=null)
        {
            Job _j=null;
            await LockAsync(async()=>{
                foreach(var j in Jobs)
                {
                    if(j.Id == id && j.JobType == resolution)
                    {
                        _j = j;
                    }
                }
                if(_j == null)
                {
                    _j = new Job();
                    _j.Id = id;
                    _j.JobType = resolution;
                    _j.Slim = semaphore;
                    Jobs.Add(_j);
                    _j.JobTask = async(p)=>{
                        switch(resolution)
                        {
                            case "PreMuxed":
                            {
                                var (video,removedByYT,off) = await GetSavedVideoAsync(id);
                                if(off && !Directory.Exists(Path.Combine(ServerPath,"PreMuxed")) && !File.Exists(Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}")))
                                                                    throw new VideoUnavailableException("We don't have access to YouTube");

                                if(removedByYT && !Directory.Exists(Path.Combine(ServerPath,"PreMuxed")) && !File.Exists(Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}")))
                                    throw new VideoUnavailableException("We dont have the video either");
                                
                                await DownloadPreMuxed(video,p);
                            }
                                break;
                            case "AudioOnly":
                            {
                                var (video,removedByYT,off) = await GetSavedVideoAsync(id);
                                if(off && !Directory.Exists(Path.Combine(ServerPath,"AudioOnly")) && !File.Exists(Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}")))
                                    throw new VideoUnavailableException("We don't have access to YouTube");
                                if(removedByYT && !Directory.Exists(Path.Combine(ServerPath,"AudioOnly")) && !File.Exists(Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}")))
                                    throw new VideoUnavailableException("We dont have the video either");
                                
                                await DownloadAudioOnly(video,p);
                            }
                                break;
                            case "VideoOnly":
                            {
                                var (video,removedByYT,off) = await GetSavedVideoAsync(id);
                                if(off && !Directory.Exists(Path.Combine(ServerPath,"VideoOnly")) && !File.Exists(Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}")))
                                    throw new VideoUnavailableException("We don't have access to YouTube");
                                if(removedByYT && !Directory.Exists(Path.Combine(ServerPath,"VideoOnly")) && !File.Exists(Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}")))
                                    throw new VideoUnavailableException("We dont have the video either");
                                
                                await DownloadVideoOnly(video,p);
                            }
                                break;
                            case "MP3":
                            {
                                var (video,removedByYT,off) = await GetSavedVideoAsync(id);
                                
                                 if(!HasConverter && !Directory.Exists(Path.Combine(ServerPath,"MP3")) && !File.Exists(Path.Combine(ServerPath,"MP3",$"{id}.mp3")))
                                    throw new OperationCanceledException("We don't have the converter.");

                                await  ConvertToMp3(video,p);
                            }
                                break;
                            case "MP4":
                            {
                                var (video,removedByYT,off) = await GetSavedVideoAsync(id);
                                
                                 if(!HasConverter && !Directory.Exists(Path.Combine(ServerPath,"MP4")) && !File.Exists(Path.Combine(ServerPath,"MP4",$"{id}.mp4")))
                                    throw new OperationCanceledException("We don't have the converter.");

                                await  ConvertToMp4(video,p);
                            }
                                break;
                            case "MKV":
                            {
                                var (video,removedByYT,off) = await GetSavedVideoAsync(id);
                                
                                 if(!HasConverter && !Directory.Exists(Path.Combine(ServerPath,"MKV")) && !File.Exists(Path.Combine(ServerPath,"MKV",$"{id}.mkv")))
                                    throw new OperationCanceledException("We don't have the converter.");

                                await  MuxToMkv(video,p);
                            }
                                break;
                        }
                    };
                    _j.Run(Jobs);
                }
                await Task.CompletedTask;
            });

            if(_j == null) throw new NullReferenceException();
            Action<double> update= (e)=>{
                try{
                progress?.Report(e);
                }catch(Exception ex){_=ex;}
            };
            _j.Update+=update;

            while(!_j.Done)
            {
                if(_j.CurrentException != null) throw _j.CurrentException;
            }
            _j.Update-=update;
        }

        private async Task DownloadAsync(ServerContext ctx)
        {
            if(ctx.QueryParams.TryGetFirst("url",out var url))
            {
                VideoId? id = VideoId.TryParse(url);
                object result = new {invalidurl=true};

                if(id.HasValue)
                {
                    (SavedVideo video,bool notavailable,bool offline)=await GetSavedVideoAsync(id.Value);
                    bool premuxed = (notavailable || offline) ? await StreamExistsAsync(video,"PreMuxed") : true;
                    bool audioonly = (notavailable || offline) ? await StreamExistsAsync(video,"AudioOnly") : true;
                    bool videoonly = (notavailable || offline) ? await StreamExistsAsync(video,"VideoOnly") : true;
                    bool converter=HasConverter; //change me
                    result = new {
                        offline,
                        notavailable,
                        premuxed,
                        audioonly,
                        videoonly,
                        converter,
                        v = id.Value.Value,
                        title = HttpUtility.HtmlEncode(video.VideoTitle),
                        channel = HttpUtility.HtmlEncode(video.ChannelTitle),
                        otherstats = $"{MakeWeb(video.Views)} View(s) {MakeWeb(video.Likes)} Like(s)<br>Uploaded: {video.UploadDate.Humanize()}<br>Length: {video.Duration.ToString()}",
                        desc=DescriptLinkUtils(video.Description ?? "").Replace("\n","<br>")
                    };
                }

                await ctx.SendTextAsync(await template.Value.RenderAsync(new{body=await pageDownload.Value.RenderAsync(result)}));
      
            }
          }
          
        private static bool StartsWithAt(string str,int indexof,string startsWith)
        {
            if(str.Length-indexof < startsWith.Length) return false;
            for(int i = 0;i<startsWith.Length;i++)
            {
                if(str[i+indexof] != startsWith[i]) return false;
            }
            return true;
        }
          internal static string DescriptLinkUtils(string url)
        {
            StringBuilder b=new StringBuilder();
            for(int i = 0;i<url.Length;i++)
            {
                if(StartsWithAt(url,i,"http:") || StartsWithAt(url,i,"https:") || StartsWithAt(url,i,"ftp:") || StartsWithAt(url,i,"ftps:") || StartsWithAt(url,i,"sftp:") || StartsWithAt(url,i,"magnet:"))
                {
                    StringBuilder b2=new StringBuilder();
                    for(;i<url.Length;i++)
                    {
                        if(url[i] == '\n' || url[i] == ' ')
                        {
                            break;
                        }
                        b2.Append(url[i]);
                    }
                    i--;
                    b.Append($"<a href=\"{HttpUtility.HtmlAttributeEncode(b2.ToString())}\">{HttpUtility.HtmlEncode(b2.ToString())}</a>");
                }
                else
                {
                    b.Append(HttpUtility.HtmlEncode(url[i]));
                }
            }
            return b.ToString();
        }
        private string MakeWeb(long no)
        {
            if(no < 1000) return $"{no}";
            if(no < 1000000) return $"{(long)(no/1000)} k";
            if(no < 1000000000) return $"{(long)(no/1000000)} M";
            if(no < 1000000000000) return $"{(long)(no/1000000000)} B";
            return $"{(long)(no/1000000000000)} T";
        }

        private async Task<bool> StreamExistsAsync(SavedVideo video, string res)
        {
            
            switch(res)
            {
                case "PreMuxed":
                {
                    string dir = Path.Combine(ServerPath,"PreMuxed");
                    if(!Directory.Exists(dir)) return false;
                    return await Task.FromResult(File.Exists(Path.Combine(dir,$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}")));
                }   
                case "VideoOnly":
                {
                    string dir = Path.Combine(ServerPath,"VideoOnly");
                    if(!Directory.Exists(dir)) return false;
                    return await Task.FromResult(File.Exists(Path.Combine(dir,$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}")));
                }   
                case "AudioOnly":
                {
                    string dir = Path.Combine(ServerPath,"AudioOnly");
                    if(!Directory.Exists(dir)) return false;
                    return await Task.FromResult(File.Exists(Path.Combine(dir,$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}")));
                }   
            }
            return false;
        }

        private async Task LockAsync(Func<Task> task)
        {
            Exception ex2=null;
            await semaphore.WaitAsync();
            try{
            await task();
            }catch(Exception ex)
            {
                ex2 = ex;
            }
            semaphore.Release();
            if(ex2 != null) throw ex2;
        }
        private async Task<byte[]> GetThumbnailAsync(VideoId id,string resolution)
        {
            string dir = Path.Combine(ServerPath,"Thumbnails",id.Value);
            string filename = Path.Combine(dir,$"{resolution}.jpg");
            await LockAsync(async()=>{
                Directory.CreateDirectory(dir);
                if(!File.Exists(filename))
                {
                    File.WriteAllBytes(filename,await HttpClient.GetByteArrayAsync($"https://s.ytimg.com/vi/{id.Value}/{resolution}.jpg"));
                }
            });
            return File.ReadAllBytes(filename);
        }
        private async Task ThumbnailAsync(ServerContext ctx)
        {
            if(ctx.QueryParams.TryGetFirst("v",out var v))
            {
                if(!ctx.QueryParams.TryGetFirst("res",out var resolution))
                {
                    resolution="sddefault";
                }
                VideoId? id = VideoId.TryParse(v);
                if(id.HasValue)
                {
                    await ctx.SendBytesAsync(await GetThumbnailAsync(id.Value,resolution),"image/jpeg");
      
                }
            }
         }

        private async Task IndexAsync(ServerContext ctx)
        {
            await ctx.SendTextAsync(await template.Value.RenderAsync(new{body=await pageIndex.Value.RenderAsync()}));
        }
        /*
public async Task<IDownloadHandle> GetDownloadHandle(VideoId id, Resolution resolution)
{

}*/
    }

    public class Job
    {
        public SemaphoreSlim Slim {get;set;}
        public Exception CurrentException {get;set;}=null;

        

        public void Run(List<Job> jobs)
        {
            Task.Run(async()=>{
                try{
                    await JobTask(new Progress<double>(e=>{
                        Update?.Invoke(e);
                    }));
                }catch(Exception ex)
                {
                    CurrentException = ex;
                    await Slim.WaitAsync();
                    jobs.Remove(this);
                    Slim.Release();
                }
                await Slim.WaitAsync();
                jobs.Remove(this);
                Slim.Release();
                Done = true;
            }).Wait(0);
        }
        public Func<IProgress<double>,Task> JobTask {get;set;}
        public event Action<double> Update;
        
        public bool Done {get;set;}

        public string JobType {get;set;}

        public string Id {get;set;}
    }

    public class SavedStreamInfo
    {
        [BsonIgnore]
        [JsonIgnore]
        public IStreamInfo Stream {
            get=>new SSI(this);
            set{
                Url=value.Url;
                Container = value.Container.Name;
                Size  = value.Size.Bytes;
                Bitrate = value.Bitrate.BitsPerSecond;
            }
        }
        public string Container {get;set;}="";
        public string Url {get;set;}="";
        public long Size {get;set;}

        public long Bitrate {get;set;}

        

        private class SSI : IStreamInfo
        {
            SavedStreamInfo info;
            public SSI(SavedStreamInfo info)
            {
                this.info = info;
            }
            public string Url => info.Url;

            public Container Container => new Container(info.Container);

            public FileSize Size => new FileSize(info.Size);

            public Bitrate Bitrate => new Bitrate(info.Bitrate);
        }
    }

    public class SavedVideo
    {
        public long Id {get;set;}
        public string VideoId {get;set;}="";
        public string VideoTitle {get;set;}="";
        public string ChannelId {get;set;}="";
        public string ChannelTitle {get;set;}="";

        public string Description {get;set;}="";

        public List<string> Tags {get;set;}=new List<string>();

        public TimeSpan Duration {get;set;}=TimeSpan.Zero;

        public long Likes {get;set;}

        public long Dislikes {get;set;}

        public long Views {get;set;}

        public DateTime UploadDate {get;set;}=DateTime.MinValue;

        public SavedStreamInfo PreMuxed {get;set;}=new SavedStreamInfo();

        public SavedStreamInfo AudioOnly {get;set;}=new SavedStreamInfo();

        public SavedStreamInfo VideoOnly {get;set;}=new SavedStreamInfo();

        public DateTime Expires {get;set;}=DateTime.Now;
        
        internal string SafeFileName(string ext)
        {
            const string BadChars = "<>?/\\*|^?\":";
            StringBuilder b=new StringBuilder();
            foreach(var c in VideoTitle)
            {
                if(BadChars.Contains(c)) continue;
                if(char.IsControl(c)) continue;
                b.Append(c);
            }
            b.Append($"-{VideoId}.{ext}");
            return b.ToString();
        }

        public static implicit operator SavedVideo(Video video)
        {
            SavedVideo video1=new SavedVideo();
            video1.VideoId = video.Id;
            video1.VideoTitle = video.Title;
            video1.ChannelId  = video.Author.ChannelId;
            video1.ChannelTitle = video.Author.ChannelTitle;
            video1.Description=video.Description;
            video1.Duration = video.Duration.HasValue ? video.Duration.Value : TimeSpan.Zero;
            video1.UploadDate = video.UploadDate.Date;
            video1.Likes = video.Engagement.LikeCount;
            video1.Dislikes  = video.Engagement.DislikeCount; //why do I bother
            video1.Views = video.Engagement.ViewCount;
            video1.Tags.AddRange(video.Keywords);
            return video1;
        } 
    }
 
    
}
