using System.Threading.Tasks;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using EnumsNET;
using PuppeteerSharp;
using Newtonsoft.Json.Linq;
using System.Threading;

namespace AyaNova.Biz
{
    internal class ReportBiz : BizObject, ISearchAbleObject
    {
        internal ReportBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
        {
            ct = dbcontext;
            UserId = currentUserId;
            UserTranslationId = userTranslationId;
            CurrentUserRoles = UserRoles;
            BizType = AyaType.Report;
        }

        internal static ReportBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
        {
            if (httpContext != null)
                return new ReportBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
            else
                return new ReportBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //EXISTS 
        internal async Task<bool> ExistsAsync(long id)
        {
            return await ct.Report.AnyAsync(z => z.Id == id);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE   
        //
        internal async Task<Report> CreateAsync(Report newObject)
        {
            await ValidateAsync(newObject, null);
            if (HasErrors)
                return null;
            else
            {
                await ct.Report.AddAsync(newObject);
                await ct.SaveChangesAsync();
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
                await SearchIndexAsync(newObject, true);
                return newObject;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DUPLICATE
        //
        internal async Task<Report> DuplicateAsync(long id)
        {
            Report dbObject = await GetAsync(id, false);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }
            Report newObject = new Report();

            CopyObject.Copy(dbObject, newObject);
            string newUniqueName = string.Empty;
            bool NotUnique = true;
            long l = 1;
            do
            {
                newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObject.Name, l++, 255);
                NotUnique = await ct.Report.AnyAsync(z => z.Name == newUniqueName);
            } while (NotUnique);
            newObject.Name = newUniqueName;
            newObject.Id = 0;
            newObject.Concurrency = 0;
            await ct.Report.AddAsync(newObject);
            await ct.SaveChangesAsync();
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
            await SearchIndexAsync(newObject, true);
            return newObject;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //IMPORT
        //
        internal async Task<bool> ImportAsync(JObject o)
        {

            Report newObject = new Report();
            var proposedName = (string)o["Name"];
            string newUniqueName = proposedName;
            bool NotUnique = true;
            long l = 1;
            do
            {
                NotUnique = await ct.Report.AnyAsync(z => z.Name == newUniqueName);
                if (NotUnique)
                    newUniqueName = Util.StringUtil.UniqueNameBuilder(proposedName, l++, 255);

            } while (NotUnique);

            newObject.Name = newUniqueName;
            newObject.Active = (bool)o["Active"];
            newObject.JsHelpers = (string)o["JsHelpers"];
            newObject.JsPrerender = (string)o["JsPrerender"];
            newObject.Notes = (string)o["Notes"];
            newObject.ObjectType = (AyaType)(int)o["ObjectType"];
            newObject.RenderType = (ReportRenderType)(int)o["RenderType"];
            newObject.Roles = (AuthorizationRoles)(int)o["Roles"];
            newObject.Style = (string)o["Style"];
            newObject.Template = (string)o["Template"];
            await ValidateAsync(newObject, null);
            if (HasErrors) return false;
            await ct.Report.AddAsync(newObject);
            await ct.SaveChangesAsync();
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
            return true;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //GET 
        //      
        internal async Task<Report> GetAsync(long id, bool logTheGetEvent = true)
        {
            var ret = await ct.Report.SingleOrDefaultAsync(z => z.Id == id);
            if (logTheGetEvent && ret != null)
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
            return ret;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE
        //
        internal async Task<Report> PutAsync(Report putObject)
        {
            Report dbObject = await ct.Report.SingleOrDefaultAsync(z => z.Id == putObject.Id);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }
            Report SnapshotOfOriginalDBObj = new Report();
            CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj);
            CopyObject.Copy(putObject, dbObject, "Id");
            ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency;
            await ValidateAsync(dbObject, SnapshotOfOriginalDBObj);
            if (HasErrors) return null;
            try
            {
                await ct.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!await ExistsAsync(putObject.Id))
                    AddError(ApiErrorCode.NOT_FOUND);
                else
                    AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
                return null;
            }
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct);
            await SearchIndexAsync(dbObject, false);
            return dbObject;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal async Task<bool> DeleteAsync(long id)
        {
            using (var transaction = await ct.Database.BeginTransactionAsync())
            {
                try
                {
                    Report dbObject = await ct.Report.SingleOrDefaultAsync(z => z.Id == id);
                    ValidateCanDelete(dbObject);
                    if (HasErrors)
                        return false;
                    if (HasErrors)
                        return false;
                    ct.Report.Remove(dbObject);
                    await ct.SaveChangesAsync();
                    await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct);
                    await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct);
                    await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
                    await transaction.CommitAsync();
                }
                catch
                {
                    //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
                    throw;
                }
                return true;
            }
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //GET LIST
        //      
        internal async Task<List<NameIdItem>> GetReportListAsync(AyaType ayType)
        {
            var rpts = await ct.Report.AsNoTracking().Where(z => z.ObjectType == ayType && z.Active == true).Select(z => new { id = z.Id, name = z.Name, roles = z.Roles }).ToListAsync();
            var ret = new List<NameIdItem>();
            foreach (var item in rpts)
            {
                if (CurrentUserRoles.HasAnyFlags(item.roles))
                {
                    ret.Add(new NameIdItem() { Name = item.name, Id = item.id });
                }
            }

            //Sort by name
            return ret.OrderBy(z => z.Name).ToList();
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //SEARCH
        //
        private async Task SearchIndexAsync(Report obj, bool isNew)
        {
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
            SearchParams.AddText(obj.Notes).AddText(obj.Name);
            if (isNew)
                await Search.ProcessNewObjectKeywordsAsync(SearchParams);
            else
                await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
        }

        public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id)
        {
            var obj = await ct.Report.SingleOrDefaultAsync(z => z.Id == id);
            var SearchParams = new Search.SearchIndexProcessObjectParameters();
            if (obj != null)
                SearchParams.AddText(obj.Notes).AddText(obj.Name);
            return SearchParams;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //VALIDATION
        //

        private async Task ValidateAsync(Report proposedObj, Report currentObj)
        {

            bool isNew = currentObj == null;

            //Name required
            if (string.IsNullOrWhiteSpace(proposedObj.Name))
                AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");

            //Name must be less than 255 characters
            if (proposedObj.Name.Length > 255)
                AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 max");

            //If name is otherwise OK, check that name is unique
            if (!PropertyHasErrors("Name"))
            {
                //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
                if (await ct.Report.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
                {
                    AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
                }
            }

        }

        private async void ValidateCanDelete(Report inObj)
        {

            //TODO: this is shitty, needs to mention notifications to be maximally useful
            if (await ct.NotifySubscription.AnyAsync(z => z.LinkReportId == inObj.Id) == true)
            {
                AddError(ApiErrorCode.INVALID_OPERATION, null, "LT:ErrorDBForeignKeyViolation");
                return;
            }

        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //REPORT DATA
        //Data fetched to return to report designer for Client report design usage

        public async Task<Newtonsoft.Json.Linq.JArray> GetReportData(ReportDataParameter reportDataParam, AuthorizationRoles overrideRoles = AuthorizationRoles.NoRole)
        {
            var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportData");
            AuthorizationRoles effectiveRoles = CurrentUserRoles;
            if (overrideRoles != AuthorizationRoles.NoRole)
                effectiveRoles = overrideRoles;

            if (!AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, reportDataParam.ObjectType))
            {
                AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {reportDataParam.ObjectType} type object");
                return null;
            }

            //Do we need to rehydrate the ID List from a DataList?
            if (reportDataParam.SelectedRowIds.Length == 0)
                reportDataParam.SelectedRowIds = await AyaNova.DataList.DataListFetcher.GetIdListResponseAsync(reportDataParam.DataListKey, reportDataParam.ListView, ct, effectiveRoles, log);

            log.LogDebug($"Instantiating biz object handler for {reportDataParam.ObjectType}");
            var biz = BizObjectFactory.GetBizObject(reportDataParam.ObjectType, ct);
            log.LogDebug($"Fetching data for {reportDataParam.SelectedRowIds.Length} {reportDataParam.ObjectType} items");
            return await ((IReportAbleObject)biz).GetReportData(reportDataParam.SelectedRowIds);
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //RENDER
        //

        public async Task<string> RenderReport(RenderReportParameter reportParam, string apiUrl, AuthorizationRoles overrideRoles = AuthorizationRoles.NoRole)
        {
            var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::RenderReport");

            //get report, vet security, see what we need before init in case of issue
            var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportParam.ReportId);
            if (report == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }

            AuthorizationRoles effectiveRoles = CurrentUserRoles;
            if (overrideRoles != AuthorizationRoles.NoRole)
                effectiveRoles = overrideRoles;


            if (!AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.ObjectType))
            {
                AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.ObjectType} type object");
                return null;
            }

            //Client meta data is required
            if (reportParam.ClientMeta == null)
            {
                AddError(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "ClientMeta parameter is missing and required to render report");
                return null;
            }


            //Default timeout for each operation of report generation
            var WaitTimeout = new WaitForFunctionOptions() { Timeout = ServerBootConfig.REPORT_RENDERING_OPERATION_TIMEOUT };


            //Get data
            var ReportData = await GetReportData(new ReportDataParameter() { ObjectType = report.ObjectType, SelectedRowIds = reportParam.SelectedRowIds, DataListKey = reportParam.DataListKey, ListView = reportParam.ListView });

            //initialization
            log.LogDebug("Initializing report system");
            var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt");
            var lo = new LaunchOptions { Headless = true };
            bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
            if (!isWindows)
            {
                log.LogDebug($"Not Windows: setting executable path for chrome to expected '/usr/bin/chromium-browser'");
                lo.ExecutablePath = "/usr/bin/chromium-browser";//this is the default path for docker based alpine dist, but maybe not others, need to make a config setting likely
                lo.Args = new string[] { "--no-sandbox" };
            }
            else
            {
                log.LogDebug($"Windows: Calling browserFetcher download async now:");
                await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
            }


            //https://stackoverflow.com/questions/53367966/puppeteer-sharp-still-appear-many-chromium-instance-in-process-task-manager-when
            log.LogDebug($"Launching headless Chrome now:");
            using (var browser = await Puppeteer.LaunchAsync(lo))
            using (var page = await browser.NewPageAsync())
            {
                try
                {


                    log.LogDebug($"Preparing page: adding base reporting scripts to page");

                    //Add Handlebars JS for compiling and presenting                    
                    //https://handlebarsjs.com/
                    await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") });

                    //add Marked for markdown processing
                    //https://github.com/markedjs/marked
                    await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") });

                    //add DOM Purify for markdown template sanitization processing
                    //https://github.com/cure53/DOMPurify
                    await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-pf.js") });

                    //add Bar code library if our bar code helper is referenced
                    //https://github.com/metafloor/bwip-js
                    if (report.Template.Contains("ayBC ") || report.JsHelpers.Contains("ayBC "))
                        await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-bc.js") });

                    //add stock helpers
                    await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") });

                    //execute to add to handlebars
                    await page.EvaluateExpressionAsync("ayRegisterHelpers();");

                    log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page");

                    //add report pre-render, helpers and style                   
                    if (string.IsNullOrWhiteSpace(report.JsPrerender))
                    {
                        report.JsPrerender = "async function ayPrepareData(reportData){return reportData;}";
                    }
                    await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender });

                    if (!string.IsNullOrWhiteSpace(report.JsHelpers))
                        await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers });

                    if (!string.IsNullOrWhiteSpace(report.Style))
                        await page.AddStyleTagAsync(new AddTagOptions() { Content = report.Style });


                    log.LogDebug($"Preparing page: adding Client meta data");

                    //Client meta data to JSON string
                    var clientMeta = reportParam.ClientMeta.ToString();

                    log.LogDebug($"Preparing page: adding Server meta data");
                    //Server meta data
                    var logo = await ct.Logo.AsNoTracking().SingleOrDefaultAsync();

                    var HasSmallLogo = "false";
                    var HasMediumLogo = "false";
                    var HasLargeLogo = "false";
                    if (logo != null)
                    {
                        if (logo.Small != null) HasSmallLogo = "true";
                        if (logo.Medium != null) HasMediumLogo = "true";
                        if (logo.Large != null) HasLargeLogo = "true";
                    }
                    var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},}}";


                    log.LogDebug($"Preparing page: adding Report meta data");

                    //Custom fields definition for report usage                  
                    string CustomFieldsTemplate = "null";
                    var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == report.ObjectType.ToString());
                    if (FormCustomization != null)
                    {
                        CustomFieldsTemplate = FormCustomization.Template;
                    }

                    //Report meta data
                    var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,ObjectType:`{report.ObjectType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportParam.DataListKey}`,ListView:`{reportParam.ListView}`,SelectedRowIds: `{string.Join(",", reportParam.SelectedRowIds)}`}}";


                    //duplicate meta data in report page wide variable for use by our internal functions
                    await page.AddScriptTagAsync(new AddTagOptions() { Content = $"var AYMETA={{ ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta}  }}" });



#if (DEBUG)
                    //view page contents
                    var pagecontent = await page.GetContentAsync();

#endif
                    //prePareData / preRender
                    var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}";
                    log.LogDebug($"Calling ayPreRender...");

                    //PRE_RENDER WITH TIMEOUT
                    //Note: defaults to 30 seconds, going to leave that in as the default
                    //in future may need to adjust but would probably do it in the context of a setting of some kind
                    await page.WaitForExpressionAsync($"ayPreRender({ReportDataObject})", WaitTimeout);

                    //compile the template
                    //NOTE: TIMEOUT?  
                    log.LogDebug($"Calling Handlebars.compile...");
                    var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);";
                    var compiledHTML = await page.EvaluateExpressionAsync<string>(compileScript);

                    //render report as HTML   
                    log.LogDebug($"Setting page content to compiled HTML");
                    await page.SetContentAsync(compiledHTML);

                    //add style (after page or it won't work)
                    if (!string.IsNullOrWhiteSpace(report.Style))
                    {
                        log.LogDebug($"Adding report template Style CSS");
                        await page.AddStyleTagAsync(new AddTagOptions { Content = report.Style });
                    }

                    string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf";
                    string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName);

                    //render to pdf and return
                    log.LogDebug($"Calling render page contents to PDF");
                    await page.PdfAsync(outputFullPath);
                    log.LogDebug($"Completed, returning results");
                    return outputFileName;
                }
                catch 
                {
                    //NOTE: in future may need to kill the chromium process if it's found to be hanging around
                    //my preliminary thinking is to keep track of the browser.process.processid above and check if still present and zap if so
                    //maybe *from* the controller (return pid as a return property from here?  Kill it if still hanging around)
                    //https://stackoverflow.com/questions/1642231/how-to-kill-a-c-sharp-process

                    //This is the error when a helper is used on the template but doesn't exist:
                    //Evaluation failed: d
                    //(it might also mean other things wrong with template)

                    // var v=await page.GetContentAsync();
                    throw;
                }
            }
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //JOB / OPERATIONS
        //


        //Other job handlers here...


        /////////////////////////////////////////////////////////////////////

    }//eoc


}//eons

