using CashLineERP.Contracts.Identity;
using System.Text;

namespace CashLineERP.Application.Identity;

public sealed class AdminUserService : IAdminUserService
{
    private readonly IAdminUserRepository _repository;

    public AdminUserService(IAdminUserRepository repository)
    {
        _repository = repository;
    }

    public IReadOnlyCollection<AdminUserResponse> GetUsers(string? userTypeCode = null, bool includeInactive = true) =>
        _repository.GetUsers(userTypeCode, includeInactive).Select(Map).ToArray();

    public AdminUserResponse? GetUser(long userId) =>
        _repository.GetUser(userId) is { } user ? Map(user) : null;

    public AdminFunctionConfigurationResponse GetFunctionConfiguration()
    {
        var internalRoles = _repository.GetRoleFunctionOptions("INTERNAL");
        var externalRoles = _repository.GetRoleFunctionOptions("EXTERNAL");
        var portalMenuSections = _repository.GetPortalMenuSections(includeDisabled: true);
        return new AdminFunctionConfigurationResponse(internalRoles, externalRoles, portalMenuSections);
    }

    public IReadOnlyCollection<AdminRoleFunctionOptionResponse> GetRoleFunctionOptions(string? userTypeCode = null) =>
        _repository.GetRoleFunctionOptions(userTypeCode);

    public AdminFunctionConfigurationResponse UpdateFunctionGovernance(UpdateAdminFunctionGovernanceRequest request, long actorUserId) =>
        _repository.UpdateFunctionGovernance(request, actorUserId);

    public bool AnyFunctionEnabled(IReadOnlyCollection<string> functionCodes) =>
        _repository.AnyFunctionEnabled(functionCodes);

    public IReadOnlyCollection<AdminUserReportRowResponse> GetUserAdministrationReportRows(string? userTypeCode = null, string? roleCode = null) =>
        GetUserAdministrationReportRowsInternal(userTypeCode, roleCode);

    private IReadOnlyCollection<AdminUserReportRowResponse> GetUserAdministrationReportRowsInternal(string? userTypeCode, string? roleCode)
    {
        var roleFilter = NormalizeRoleFilter(roleCode);
        return _repository.GetUsers(userTypeCode, includeInactive: true)
            .SelectMany(user =>
            {
                var roles = user.RoleCodes.Count == 0 ? new[] { "NO_ACTIVE_ROLE" } : user.RoleCodes;
                return roles.Select(role => new AdminUserReportRowResponse(
                    user.DisplayName,
                    user.UserTypeCode,
                    role,
                    user.DateOfJoiningUtc,
                    user.DateOfSuspensionUtc,
                    user.LatestActionCode,
                    user.LastAdminActionReason));
            })
            .Where(row => roleFilter.Count == 0 || roleFilter.Contains(row.Role))
            .OrderBy(row => row.UserType)
            .ThenBy(row => row.UserName)
            .ThenBy(row => row.Role)
            .ToArray();
    }

    private static HashSet<string> NormalizeRoleFilter(string? roleCode)
    {
        if (string.IsNullOrWhiteSpace(roleCode))
        {
            return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        }

        var normalized = roleCode.Trim();
        if (string.Equals(normalized, "SYSTEM_OWNER_SCOPE", StringComparison.OrdinalIgnoreCase))
        {
            return new HashSet<string>(new[]
            {
                "SUPER_ADMIN",
                "ACCOUNTING_OFFICE_USER",
                "ACCOUNTING_OFFICE_SUPERVISOR"
            }, StringComparer.OrdinalIgnoreCase);
        }

        var split = normalized
            .Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(entry => !string.IsNullOrWhiteSpace(entry))
            .ToArray();
        if (split.Length <= 1)
        {
            return new HashSet<string>(new[] { normalized }, StringComparer.OrdinalIgnoreCase);
        }

        return new HashSet<string>(split, StringComparer.OrdinalIgnoreCase);
    }

    public string ExportUserAdministrationReportExcel(long actorUserId, string? userTypeCode = null, string? roleCode = null)
    {
        _repository.RecordUserAdministrationReportExport(actorUserId, "EXCEL", userTypeCode, roleCode);
        var builder = new StringBuilder();
        builder.AppendLine("User Name\tUser Type\tRole\tDate of Joining\tDate of Suspension\tLatest Action\tLatest Action Reason");
        foreach (var row in GetUserAdministrationReportRows(userTypeCode, roleCode))
        {
            builder
                .Append(EscapeTab(row.UserName)).Append('\t')
                .Append(EscapeTab(row.UserType)).Append('\t')
                .Append(EscapeTab(row.Role)).Append('\t')
                .Append(row.DateOfJoiningUtc.ToString("yyyy-MM-dd HH:mm:ss")).Append('\t')
                .Append(row.DateOfSuspensionUtc?.ToString("yyyy-MM-dd HH:mm:ss") ?? string.Empty).Append('\t')
                .Append(EscapeTab(row.LatestAction ?? string.Empty)).Append('\t')
                .Append(EscapeTab(row.LatestActionReason ?? string.Empty))
                .AppendLine();
        }

        return builder.ToString();
    }

    public byte[] ExportUserAdministrationReportPdf(long actorUserId, string? userTypeCode = null, string? roleCode = null)
    {
        _repository.RecordUserAdministrationReportExport(actorUserId, "PDF", userTypeCode, roleCode);
        var rows = GetUserAdministrationReportRows(userTypeCode, roleCode);
        var content = BuildPdfContent(rows);
        var objects = new[]
        {
            "<< /Type /Catalog /Pages 2 0 R >>",
            "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 842 595] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
            "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
            $"<< /Length {Encoding.ASCII.GetByteCount(content)} >>\nstream\n{content}endstream"
        };

        var builder = new StringBuilder();
        var offsets = new List<int> { 0 };
        builder.AppendLine("%PDF-1.4");
        builder.AppendLine("% CashLine ERP user administration report");
        for (var index = 0; index < objects.Length; index++)
        {
            offsets.Add(Encoding.ASCII.GetByteCount(builder.ToString()));
            builder.AppendLine($"{index + 1} 0 obj");
            builder.AppendLine(objects[index]);
            builder.AppendLine("endobj");
        }

        var xrefOffset = Encoding.ASCII.GetByteCount(builder.ToString());
        builder.AppendLine("xref");
        builder.AppendLine($"0 {objects.Length + 1}");
        builder.AppendLine("0000000000 65535 f ");
        foreach (var offset in offsets.Skip(1))
        {
            builder.AppendLine($"{offset:0000000000} 00000 n ");
        }

        builder.AppendLine("trailer << /Root 1 0 R /Size 6 >>");
        builder.AppendLine("startxref");
        builder.AppendLine(xrefOffset.ToString());
        builder.AppendLine("%%EOF");
        return Encoding.ASCII.GetBytes(builder.ToString());
    }

    public AdminUserResponse CreateUser(CreateAdminManagedUserRequest request, long actorUserId) =>
        Map(_repository.CreateUser(request, actorUserId));

    public AdminUserResponse ReplaceRoles(long userId, UpdateAdminManagedUserRolesRequest request, long actorUserId) =>
        Map(_repository.ReplaceRoles(userId, request.RoleCodes, request.FunctionCodes, actorUserId));

    public AdminUserResponse SuspendUser(long userId, AdminUserLifecycleActionRequest request, long actorUserId) =>
        Map(_repository.SuspendUser(userId, actorUserId, request.Reason));

    public AdminUserResponse HoldUser(long userId, AdminUserLifecycleActionRequest request, long actorUserId) =>
        Map(_repository.HoldUser(userId, actorUserId, request.Reason));

    public AdminUserResponse ReinstateUser(long userId, AdminUserLifecycleActionRequest request, long actorUserId) =>
        Map(_repository.ReinstateUser(userId, actorUserId, request.Reason));

    public AdminUserResponse RevokeUser(long userId, AdminUserLifecycleActionRequest request, long actorUserId) =>
        Map(_repository.RevokeUser(userId, actorUserId, request.Reason));

    public AdminUserResponse DeleteUser(long userId, AdminUserLifecycleActionRequest request, long actorUserId) =>
        Map(_repository.DeleteUser(userId, actorUserId, request.Reason));

    private static AdminUserResponse Map(AdminUserView user) =>
        new(
            user.UserId,
            user.LoginName,
            user.DisplayName,
            user.UserTypeCode,
            user.Email,
            user.BranchId,
            user.PartyId,
            user.IsActive,
            user.IsLocked,
            user.RoleCodes,
            user.FunctionCodes,
            ResolveLifecycleStatus(user),
            user.DateOfJoiningUtc,
            user.DateOfSuspensionUtc,
            user.LastAdminActionReason);

    private static string ResolveLifecycleStatus(AdminUserView user)
    {
        if (!user.IsActive && user.IsLocked && user.RoleCodes.Count == 0)
        {
            return "DELETED";
        }

        if (!user.IsActive)
        {
            return "SUSPENDED";
        }

        if (user.IsLocked && user.RoleCodes.Count == 0)
        {
            return "ACCESS_REVOKED";
        }

        return user.IsLocked ? "ON_HOLD" : "ACTIVE";
    }

    private static string EscapeTab(string value) =>
        value.Replace('\t', ' ').Replace("\r", " ").Replace("\n", " ");

    private static string BuildPdfContent(IReadOnlyCollection<AdminUserReportRowResponse> rows)
    {
        var builder = new StringBuilder();
        builder.AppendLine("BT /F1 12 Tf 36 552 Td (CashLine ERP - User Administration Report) Tj ET");
        builder.AppendLine("BT /F1 8 Tf 36 532 Td (User | Type | Role | Joined | Suspended | Action | Reason) Tj ET");
        var y = 516;
        foreach (var row in rows.Take(28))
        {
            var line = $"{row.UserName} | {row.UserType} | {row.Role} | {row.DateOfJoiningUtc:yyyy-MM-dd} | {row.DateOfSuspensionUtc:yyyy-MM-dd} | {row.LatestAction} | {row.LatestActionReason}";
            builder.AppendLine($"BT /F1 7 Tf 36 {y} Td ({EscapePdf(line)}) Tj ET");
            y -= 16;
        }

        return builder.ToString();
    }

    private static string EscapePdf(string value) =>
        value
            .Replace("\\", "\\\\")
            .Replace("(", "\\(")
            .Replace(")", "\\)");
}
