using CashLineERP.Application.Identity;
using CashLineERP.Domain.Identity;
using CashLineERP.SharedKernel.Identity;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class OnboardingAccessProvisioningRepository : IOnboardingAccessProvisioningRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public OnboardingAccessProvisioningRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public OnboardingAccessProvisioningResult EnsureExternalPartyAccess(
        long partyId,
        string expectedPartyType,
        string roleCode,
        long actorUserId)
    {
        var normalizedPartyType = expectedPartyType.Trim().ToUpperInvariant();
        var normalizedRoleCode = roleCode.Trim().ToUpperInvariant();

        var party = _dbContext.BusinessParties.SingleOrDefault(entry => entry.Id == partyId)
            ?? throw new InvalidOperationException($"Business party '{partyId}' was not found for onboarding access provisioning.");

        if (!party.IsActive)
        {
            throw new InvalidOperationException("Onboarding access provisioning requires an active business party.");
        }

        if (!string.Equals(party.PartyType, normalizedPartyType, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException($"Onboarding access provisioning expected a {normalizedPartyType} party.");
        }

        var role = _dbContext.Roles
            .AsNoTracking()
            .SingleOrDefault(entry => entry.RoleCode == normalizedRoleCode && entry.IsActive)
            ?? throw new InvalidOperationException($"Role '{normalizedRoleCode}' was not found for onboarding access provisioning.");

        var existingUser = _dbContext.Users
            .Include(entry => entry.Roles)
            .SingleOrDefault(entry =>
                entry.PartyId == partyId
                && entry.UserTypeCode == "EXTERNAL"
                && entry.IsActive);

        if (existingUser is not null)
        {
            EnsureRoleAssignment(existingUser, role.Id);
            _dbContext.SaveChanges();

            return new OnboardingAccessProvisioningResult(
                false,
                existingUser.Id,
                party.Id,
                party.PartyCode,
                party.PartyName,
                party.PartyType,
                existingUser.LoginName,
                existingUser.Email,
                party.MobileNumber,
                null,
                normalizedRoleCode);
        }

        if (string.IsNullOrWhiteSpace(party.Email) && string.IsNullOrWhiteSpace(party.MobileNumber))
        {
            throw new InvalidOperationException("Onboarding activation requires at least one approved communication destination: email or mobile number.");
        }

        var nextUserId = (_dbContext.Users.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var nextUserRoleId = (_dbContext.UserRoles.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var loginName = BuildUniqueLoginName(party, normalizedPartyType);
        var temporaryPassword = GenerateTemporaryPassword();
        var user = new ApplicationUser(
            nextUserId,
            loginName,
            $"{party.PartyName} Portal Admin",
            "EXTERNAL",
            PasswordSecurity.HashPassword(temporaryPassword),
            email: party.Email,
            branchId: null,
            partyId: party.Id);

        var assignment = new UserRole(user.Id, role.Id, DateTime.UtcNow);
        assignment.GetType().GetProperty("Id")?.SetValue(assignment, nextUserRoleId);

        _dbContext.Users.Add(user);
        _dbContext.UserRoles.Add(assignment);
        user.Touch(actorUserId);
        _dbContext.SaveChanges();

        return new OnboardingAccessProvisioningResult(
            true,
            user.Id,
            party.Id,
            party.PartyCode,
            party.PartyName,
            party.PartyType,
            user.LoginName,
            user.Email,
            party.MobileNumber,
            temporaryPassword,
            normalizedRoleCode);
    }

    private void EnsureRoleAssignment(ApplicationUser user, long roleId)
    {
        if (user.Roles.Any(entry => entry.RoleId == roleId && entry.IsActive))
        {
            return;
        }

        var nextUserRoleId = (_dbContext.UserRoles.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var assignment = new UserRole(user.Id, roleId, DateTime.UtcNow);
        assignment.GetType().GetProperty("Id")?.SetValue(assignment, nextUserRoleId);
        _dbContext.UserRoles.Add(assignment);
    }

    private string BuildUniqueLoginName(Domain.Parties.BusinessParty party, string partyType)
    {
        var prefix = string.Equals(partyType, "SUPPLIER", StringComparison.OrdinalIgnoreCase) ? "SUP" : "CUS";
        var normalizedPartyCode = new string(party.PartyCode
            .ToUpperInvariant()
            .Where(char.IsLetterOrDigit)
            .ToArray());
        var candidate = $"{prefix}_{normalizedPartyCode}_{party.Id}";
        if (!_dbContext.Users.AsNoTracking().Any(entry => entry.LoginName == candidate))
        {
            return candidate;
        }

        var suffix = 1;
        while (true)
        {
            var withSuffix = $"{candidate}_{suffix}";
            if (!_dbContext.Users.AsNoTracking().Any(entry => entry.LoginName == withSuffix))
            {
                return withSuffix;
            }

            suffix++;
        }
    }

    private static string GenerateTemporaryPassword()
    {
        const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
        Span<char> buffer = stackalloc char[10];
        for (var index = 0; index < buffer.Length; index++)
        {
            buffer[index] = chars[RandomNumberGenerator.GetInt32(chars.Length)];
        }

        return $"CashLine!{new string(buffer)}";
    }
}
