using CashLineERP.Application.Identity;
using CashLineERP.Domain.Identity;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class AccountingOfficeClientAssignmentRepository : IAccountingOfficeClientAssignmentRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<AccountingOfficeClientAssignmentView> GetAssignments(long accountingOfficeUserId, bool includeInactive = false)
    {
        EnsureAccountingOfficeUser(accountingOfficeUserId);

	        return _dbContext.AccountingOfficeClientAssignments
	            .AsNoTracking()
	            .Where(entry => entry.AccountingOfficeUserId == accountingOfficeUserId)
	            .Where(entry => includeInactive || entry.IsActive)
	            .Join(
	                _dbContext.BusinessParties.AsNoTracking(),
	                assignment => assignment.ClientPartyId,
	                party => party.Id,
	                (assignment, party) => new { assignment, party })
	            .OrderBy(entry => entry.party.PartyType)
	            .ThenBy(entry => entry.party.PartyCode)
	            .Select(entry => new AccountingOfficeClientAssignmentView(
	                entry.assignment.AccountingOfficeUserId,
	                entry.party.Id,
	                entry.party.PartyCode,
	                entry.party.PartyName,
	                entry.party.PartyType,
	                entry.party.Email,
	                entry.party.MobileNumber,
	                entry.assignment.CreatedAtUtc,
	                entry.assignment.CreatedByUserId))
	            .ToArray();
	    }

    public AccountingOfficeClientAssignmentView Assign(long accountingOfficeUserId, long partyId, long actorUserId)
    {
        EnsureAccountingOfficeUser(accountingOfficeUserId);
        var party = EnsureActiveParty(partyId);

        var existing = _dbContext.AccountingOfficeClientAssignments
            .SingleOrDefault(entry => entry.AccountingOfficeUserId == accountingOfficeUserId && entry.ClientPartyId == partyId);

        if (existing is null)
        {
            existing = new AccountingOfficeClientAssignment(accountingOfficeUserId, partyId, actorUserId);
            _dbContext.AccountingOfficeClientAssignments.Add(existing);
        }
        else if (!existing.IsActive)
        {
            existing.Reinstate(actorUserId);
        }

        _dbContext.SaveChanges();

        return new AccountingOfficeClientAssignmentView(
            accountingOfficeUserId,
            party.Id,
            party.PartyCode,
            party.PartyName,
            party.PartyType,
            party.Email,
            party.MobileNumber,
            existing.CreatedAtUtc,
            existing.CreatedByUserId);
    }

    public void Revoke(long accountingOfficeUserId, long partyId, long actorUserId)
    {
        EnsureAccountingOfficeUser(accountingOfficeUserId);

        var assignment = _dbContext.AccountingOfficeClientAssignments
            .SingleOrDefault(entry => entry.AccountingOfficeUserId == accountingOfficeUserId && entry.ClientPartyId == partyId && entry.IsActive);

        if (assignment is null)
        {
            return;
        }

        assignment.Revoke(actorUserId);
        _dbContext.SaveChanges();
    }

    public bool IsAssigned(long accountingOfficeUserId, long partyId) =>
        _dbContext.AccountingOfficeClientAssignments
            .AsNoTracking()
            .Any(entry => entry.AccountingOfficeUserId == accountingOfficeUserId && entry.ClientPartyId == partyId && entry.IsActive);

    public long? ResolvePrimaryOfficeUserId(long clientPartyId) =>
        _dbContext.AccountingOfficeClientAssignments
            .AsNoTracking()
            .Where(entry => entry.ClientPartyId == clientPartyId && entry.IsActive)
            .OrderBy(entry => entry.AccountingOfficeUserId)
            .Select(entry => (long?)entry.AccountingOfficeUserId)
            .FirstOrDefault();

    private void EnsureAccountingOfficeUser(long accountingOfficeUserId)
    {
	        var hasAccountingOfficeRole = _dbContext.UserRoles
	            .AsNoTracking()
	            .Where(entry => entry.UserId == accountingOfficeUserId && entry.IsActive)
	            .Join(
	                _dbContext.Roles.AsNoTracking(),
	                userRole => userRole.RoleId,
	                role => role.Id,
	                (userRole, role) => role.RoleCode)
	            // SQLite EF provider cannot translate string.Equals(StringComparison).
	            .Any(roleCode =>
	                (roleCode ?? string.Empty).ToUpper() == "ACCOUNTING_OFFICE_USER"
	                || (roleCode ?? string.Empty).ToUpper() == "ACCOUNTING_OFFICE_SUPERVISOR");

        if (!hasAccountingOfficeRole)
        {
            throw new InvalidOperationException($"User '{accountingOfficeUserId}' is not an Accounting Office governed user.");
        }
    }

    private CashLineERP.Domain.Parties.BusinessParty EnsureActiveParty(long partyId) =>
        _dbContext.BusinessParties
            .SingleOrDefault(entry => entry.Id == partyId && entry.IsActive)
        ?? throw new InvalidOperationException($"Active business party '{partyId}' was not found.");
}
