using System.Diagnostics;
using Fast.Models;
using Fast.Shared.Logic.FileService;
using Fast.Web.Logic;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace Fast.Web.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ReportController : ODataController
{
    private readonly MyDbContext db;
    private readonly IFileService fileService;
    private readonly AuthorizationHelper authHelper;
    private readonly IWebHostEnvironment env;
    private const int testDataSourceId = 0;

    public ReportController(MyDbContext context, IWebHostEnvironment env)
    {
        db = context;
        this.env = env;
        this.authHelper = new AuthorizationHelper(Main.IsAuthenticationEnabled);
        var config = new FileServiceConfig(env.ContentRootPath);
        fileService = new FileService(config);
    }

    [Permission("Report Center", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetReports(bool showAll)
    {
        const int itemsToPreview = 3;

        bool hasReportModifyPermission = await authHelper.IsAuthorizedAsync(User, "Report Center", PermissionType.Modify);
        if (!hasReportModifyPermission)
            showAll = false;

        List<ReportListItem> reports = new();

        int appUserId = Util.GetAppUserId(User);

        var userGroupIds = await (from q in db.SecurityUserGroups where q.UserId == appUserId select q.SecurityGroupId.ToString()).AsNoTracking().ToListAsync();
        var userIdStr = appUserId.ToString();
        char[] separator = { ',' };

        var userReportFilters = await (
            from q in db.ReportFilters
            where q.UserId == appUserId
            select new
            {
                FilterId = q.Id,
                FilterName = q.Name,
                q.ReportId,
                q.IsSelected,
                q.ReportFilterParameters
            }
        ).AsNoTracking().ToListAsync();

        var selectedFilterIdsDic = userReportFilters.Where(x => x.IsSelected == true).ToDictionary(x => x.ReportId, x => x.FilterId);
        var filtersLookup = userReportFilters.ToLookup(x => x.ReportId, x => new ReportListItemFilter { Id = x.FilterId, Name = x.FilterName, HasRequiredFilterParams = true });
        var filterParamsDic = userReportFilters.ToLookup(x => x.ReportId, x => x.ReportFilterParameters).ToDictionary(x => x.Key, x => x.SelectMany(y => y).ToList());

        //using ToList() to retrieve all and then perform filters locally
        var reportsLocal = await (
            from q in db.Reports
            where Main.IsDevEnvOrDb || q.DataSourceIds != testDataSourceId.ToString()
            select new ReportLocal
            {
                Id = q.Id,
                Name = q.Name,
                SecurityGroupIds = q.SecurityGroupIds,
                SecurityUserIds = q.SecurityUserIds,
                DataSourceIds = q.DataSourceIds,
                TemplateFileNameOriginal = q.TemplateFileNameOriginal,
                SaveAsPdf = q.SaveAsPdf
            }
        ).ToListAsync();

        foreach (var reportLocal in reportsLocal)
        {
            reportLocal.SelectedFilterId = selectedFilterIdsDic.ContainsKey(reportLocal.Id) ? selectedFilterIdsDic[reportLocal.Id] : 0;
            reportLocal.Filters = filtersLookup.Contains(reportLocal.Id) ? filtersLookup[reportLocal.Id].ToList() : new List<ReportListItemFilter>();
            reportLocal.FilterParams = filterParamsDic.ContainsKey(reportLocal.Id) ? filterParamsDic[reportLocal.Id] : new List<ReportFilterParameter>();
        }

        var reportsLocalFiltered = (
            from q in reportsLocal
            where
                showAll == true ||
                (q.SecurityGroupIds != null && q.SecurityGroupIds.Split(separator).Intersect(userGroupIds).Any()) ||
                (q.SecurityUserIds != null && q.SecurityUserIds.Split(separator).Contains(userIdStr))
            orderby q.Name
            select q
        ).ToList();

        var dataSources = await (from q in db.ReportDataSources where Main.IsDevEnvOrDb || q.Id != testDataSourceId select q).AsNoTracking().ToDictionaryAsync(x => x.Id);

        foreach (var item in reportsLocalFiltered)
        {
            List<int> intDataSourceIds = item.DataSourceIds.Split(",").Select(x => int.Parse(x)).ToList();
            ILookup<Tuple<int, int>, ReportFilterParameter> filterParamsByFilterIdAndDataSourceId = item.FilterParams.ToLookup(x => Tuple.Create(x.FilterId, x.DataSourceId));

            //if IsFilterRequired for any dataSource then set hasRequiredFilter true
            bool hasRequiredFilter = false;
            foreach (int dataSourceId in intDataSourceIds)
            {
                if (dataSources[dataSourceId].IsFilterRequired)
                    hasRequiredFilter = true;
            }

            var reportItem = new ReportListItem
            {
                Id = item.Id,
                Name = item.Name,
                SelectedFilterId = item.SelectedFilterId == 0 ? (int?)null : item.SelectedFilterId,
                Filters = item.Filters.OrderBy(x => x.Name).ToList(),
                DataSourceSet = new Dictionary<int, List<DataSourceItem>>(),
                FileExtension = Path.GetExtension(item.TemplateFileNameOriginal),
                SaveAsPdf = item.SaveAsPdf,
                HasRequiredFilter = hasRequiredFilter
            };

            //create a DaraSourceItem for when No Filter is selected
            List<DataSourceItem> dsItemsNofilter = new();
            foreach (var dataSourceId in intDataSourceIds)
            {
                var dataSource = dataSources[dataSourceId];

                DataSourceItem dsItem = new()
                {
                    id = dataSource.Id,
                    name = dataSource.Name,
                    itemFilters = new List<DataSourceItemFilter>()
                };

                DataSourceItemFilter dsItemFilter = new() { FilterHeader = "No Filters Applied" };
                dsItem.itemFilters.Add(dsItemFilter);

                dsItemsNofilter.Add(dsItem);
            }
            //DataSourceSet with filter.Id set to 0 for no selected filter
            reportItem.DataSourceSet[0] = dsItemsNofilter;

            //create DaraSourceItems for existing Filters
            foreach (var filter in reportItem.Filters)
            {
                List<DataSourceItem> dsItems = new();

                foreach (var dataSourceId in intDataSourceIds)
                {
                    var dataSource = dataSources[dataSourceId];

                    DataSourceItem dsItem = new()
                    {
                        id = dataSource.Id,
                        name = dataSource.Name,
                        itemFilters = new List<DataSourceItemFilter>()
                    };

                    var filterParams = filterParamsByFilterIdAndDataSourceId[Tuple.Create(filter.Id, dataSource.Id)].OrderBy(x => x.Name);

                    if (dataSources[dataSourceId].IsFilterRequired && !filterParams.Any())
                        filter.HasRequiredFilterParams = false;

                    foreach (var param in filterParams)
                    {
                        DataSourceItemFilter dsItemFilter = new() { FilterHeader = param.Name };
                        var paramSplit = param.Preview.Split("\u2022");
                        for (int i = 0; i < itemsToPreview && i < paramSplit.Length; i++)
                        {
                            var splitPiece = paramSplit[i];
                            dsItemFilter.FilterValues.Add(splitPiece);
                        }

                        if (paramSplit.Length > itemsToPreview)
                        {
                            int additionalItemCounter = paramSplit.Length - itemsToPreview;
                            string additionalItemStr = "+" + additionalItemCounter + " more";
                            dsItemFilter.FilterValues.Add(additionalItemStr);
                        }

                        dsItem.itemFilters.Add(dsItemFilter);
                    }

                    if (!filterParams.Any())
                    {
                        DataSourceItemFilter dsItemFilter = new() { FilterHeader = "No Filters Applied" };
                        dsItem.itemFilters.Add(dsItemFilter);
                    }

                    dsItems.Add(dsItem);
                }

                reportItem.DataSourceSet[filter.Id] = dsItems;
            }

            reports.Add(reportItem);
        }

        return Ok(reports);
    }

    [Permission("Report Center", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetRequiredData()
    {
        bool hasReportModifyPermission = await authHelper.IsAuthorizedAsync(User, "Report Center", PermissionType.Modify);
        var securityGroups = await (from q in db.SecurityGroups orderby q.Name select new IdName(q.Id, q.Name)).AsNoTracking().ToListAsync();
        var userInfos = await (from q in db.AppUsers orderby q.DisplayName select new IdName(q.Id, q.DisplayName)).AsNoTracking().ToListAsync();
        var dataSources = await (from q in db.ReportDataSources where Main.IsDevEnvOrDb || q.Id != testDataSourceId orderby q.Name select new IdName(q.Id, q.Name)).AsNoTracking().ToListAsync();

        var result = new { hasReportModifyPermission, securityGroups, userInfos, dataSources };

        return Ok(result);
    }

    [Permission("Report Center", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> GetReportDetail(int reportId)
    {
        char[] separator = { ',' };

        var report = await (
            from q in db.Reports
            where q.Id == reportId
            select new { q.Id, q.Name, q.DataSourceIds, q.TemplateFileNameOriginal, q.TemplateFileNameOnDisk, q.SecurityGroupIds, q.SecurityUserIds, q.Notes }
        ).AsNoTracking().FirstAsync();

        ReportDetail reportDetail = new()
        {
            Id = report.Id,
            Name = report.Name,
            DataSourceIds = Util.String.ConvertCommaSeparatedIdsToList(report.DataSourceIds),
            TemplateFileNameOriginal = report.TemplateFileNameOriginal,
            TemplateFileNameOnDisk = report.TemplateFileNameOnDisk,
            SecurityGroupIds = Util.String.ConvertCommaSeparatedIdsToList(report.SecurityGroupIds),
            SecurityUserIds = Util.String.ConvertCommaSeparatedIdsToList(report.SecurityUserIds),
            Notes = report.Notes
        };

        return Ok(reportDetail);
    }

    [Permission("Report Center", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> SaveReportDetail(ReportDetail reportDetail)
    {
        Report? dbItem = await (from q in db.Reports where q.Id == reportDetail.Id select q).FirstOrDefaultAsync();

        if (dbItem == null) //if the item does not exist then add it
        {
            dbItem = new Report();
            db.Reports.Add(dbItem);
        }

        dbItem.Name = reportDetail.Name ?? "";
        dbItem.TemplateFileNameOriginal = reportDetail.TemplateFileNameOriginal ?? "";
        dbItem.TemplateFileNameOnDisk = reportDetail.TemplateFileNameOnDisk ?? "";
        dbItem.DataSourceIds = Util.String.ConvertIdsListToString(reportDetail.DataSourceIds);
        dbItem.SecurityGroupIds = Util.String.ConvertIdsListToString(reportDetail.SecurityGroupIds);
        dbItem.SecurityUserIds = Util.String.ConvertIdsListToString(reportDetail.SecurityUserIds);
        dbItem.Notes = reportDetail.Notes;

        int count = await db.SaveChangesAsync();

        return Ok(count);
    }

    [Permission("Report Center", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> SaveReportTemplate(IFormFile file)
    {
        var fileNameOnDisk = Util.String.GetNewGuid() + Path.GetExtension(file.FileName);
        var uploadedFileName = await fileService.UploadFileAsync("ReportTemplates", fileNameOnDisk, file);

        if (uploadedFileName == null)
            return BadRequest();

        return new JsonResult(new
        {
            fileNameOriginal = file.FileName,
            fileNameOnDisk
        });
    }

    [Permission("Report Center", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> DownloadReportTemplate(string fileNameOnDisk)
    {
        var fileResponse = await fileService.DownloadFileAsync("ReportTemplates", fileNameOnDisk);
        return File(fileResponse.Stream, fileResponse.ContentType, fileResponse.FileName);
    }


    [Permission("Report Center", PermissionType.Modify)]
    [Route("[action]/{id}")]
    public async Task<IActionResult> DeleteReport(int id)
    {
        Report? dbItem = await db.Reports.Where(x => x.Id == id).FirstOrDefaultAsync();
        if (dbItem != null)
        {
            db.Entry(dbItem).State = EntityState.Deleted;
            await db.SaveChangesAsync();
        }

        return Ok();
    }

    [Permission("Report Center", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> RunReport(int reportId, int? filterId, bool saveAsPdf)
    {
        async Task<FileStreamResult> runReport()
        {
            int appUserId = Util.GetAppUserId(User);

            var runner = new ReportRunner(db, env.ContentRootPath);
            var streamAndFileName = await runner.Run(reportId, filterId ?? 0, appUserId, saveAsPdf);

            return File(streamAndFileName.Stream, "application/octet-stream", streamAndFileName.FileName);
        }

        if (Main.IsDevEnv)
        {
            var sw = Stopwatch.StartNew();
            var result = await runReport();
            Debug.WriteLine($"RunReport(): {sw.ElapsedMilliseconds} ms");
            return result;
        }
        else
        {
            try
            {
                return await runReport();
            }
            catch (Exception ex)
            {
                string messagePrefix = $"Report ID: {reportId}";
                messagePrefix += Environment.NewLine + $"Filter ID: {filterId}";
                var bytes = Util.GetExceptionFilesBytes(ex, messagePrefix);
                return File(bytes, "text/plain", "Error.txt");
            }
        }
    }

    [Permission("Report Center", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> SetSaveAsPdf(int reportId, bool saveAsPdf)
    {
        var report = await db.Reports.Where(x => x.Id == reportId).FirstOrDefaultAsync();
        if (report != null)
        {
            report.SaveAsPdf = saveAsPdf;
            await db.SaveChangesAsync();
        }

        return Ok();
    }

}
