using System.Security.Claims;
using foodmarket.Application.Common;
using foodmarket.Domain.Organizations;
using foodmarket.Infrastructure.Identity;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace foodmarket.Api.Controllers.SuperAdmin;

/// <summary>SuperAdmin: управление сотрудниками любой организации в обход
/// tenant-фильтра и обычных гардов (роль главного администратора, self-delete
/// и т.д.). Все мутации обязательно сопровождаются reason (минимум 10
/// символов) и пишутся в super_admin_audit_log.
///
/// Tenant-эндпойнты `/api/organization/employees/*` оставляем для обычных
/// админов организации с их гардами; этот контроллер — только для
/// Супер-администратора платформы.</summary>
[ApiController]
[Authorize(Roles = "SuperAdmin")]
[Route("api/super-admin/organizations/{orgId:guid}/employees")]
public class SuperAdminEmployeesController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly UserManager<User> _userMgr;

    public SuperAdminEmployeesController(AppDbContext db, UserManager<User> userMgr)
    {
        _db = db; _userMgr = userMgr;
    }

    public record EmployeeRow(
        Guid Id, Guid? UserId, string LastName, string FirstName, string? MiddleName,
        string? Position, string? Email, string? Phone,
        Guid RoleId, string RoleName, bool IsRoleSystem,
        bool IsActive, DateTime? FiredAt,
        bool HasAccount, bool AccountActive, DateTime? LastLoginAt,
        bool IsOwner);

    public record EmployeeDetail(
        Guid Id, Guid OrganizationId, Guid? UserId,
        string LastName, string FirstName, string? MiddleName,
        string? Position, string? Email, string? Phone,
        decimal? Salary, string? TaxNumber, string? Description, string? ImageUrl,
        Guid RoleId, string RoleName,
        bool IsActive, DateTime? FiredAt,
        IReadOnlyList<Guid> RetailPointIds,
        bool IsOwner, bool HasAccount, bool AccountActive, string? AccountEmail);

    public record EmployeeInput(
        string LastName, string FirstName, string? MiddleName,
        string? Position, string? Email, string? Phone,
        decimal? Salary, string? TaxNumber, string? Description, string? ImageUrl,
        Guid RoleId, bool IsActive,
        IReadOnlyList<Guid>? RetailPointIds);

    public record CreateInput(
        string Reason,
        string LastName, string FirstName, string? MiddleName,
        string? Position, string? Email, string? Phone,
        Guid RoleId, bool IsActive,
        IReadOnlyList<Guid>? RetailPointIds,
        bool CreateAccount, string? AccountEmail);

    public record UpdateInput(string Reason, EmployeeInput Employee);

    public record ResetPasswordInput(string Reason);
    public record ResetPasswordResult(string Email, string TempPassword);

    public record ToggleActiveInput(string Reason, bool IsActive);

    public record ToggleAccountActiveInput(string Reason, bool IsActive);

    [HttpGet]
    public async Task<ActionResult<PagedResult<EmployeeRow>>> List(
        Guid orgId, [FromQuery] PagedRequest req,
        [FromQuery] bool includeInactive = true,
        CancellationToken ct = default)
    {
        var orgExists = await _db.Organizations.IgnoreQueryFilters().AnyAsync(o => o.Id == orgId, ct);
        if (!orgExists) return NotFound();

        var ownerUserId = await _db.Organizations.IgnoreQueryFilters()
            .Where(o => o.Id == orgId).Select(o => o.AccountOwnerUserId).FirstOrDefaultAsync(ct);

        var q = _db.Employees.IgnoreQueryFilters().AsNoTracking()
            .Include(e => e.Role)
            .Where(e => e.OrganizationId == orgId);
        if (!includeInactive) q = q.Where(e => e.IsActive);
        if (!string.IsNullOrWhiteSpace(req.Search))
        {
            var s = req.Search.Trim().ToLower();
            q = q.Where(e =>
                e.LastName.ToLower().Contains(s) || e.FirstName.ToLower().Contains(s) ||
                (e.Email != null && e.Email.ToLower().Contains(s)));
        }
        var total = await q.CountAsync(ct);

        // Загружаем + проецируем после материализации, чтобы JOIN на users
        // не упёрся в IgnoreQueryFilters (Identity-таблица не tenant-scoped,
        // но избегаем сложного group-join'а в EF выражениях).
        var rows = await q.OrderByDescending(e => e.IsActive).ThenBy(e => e.LastName)
            .Skip(req.Skip).Take(req.Take).ToListAsync(ct);
        var userIds = rows.Where(r => r.UserId != null).Select(r => r.UserId!.Value).ToList();
        var userMap = userIds.Count == 0
            ? new Dictionary<Guid, User>()
            : await _db.Users.IgnoreQueryFilters().Where(u => userIds.Contains(u.Id))
                .ToDictionaryAsync(u => u.Id, ct);

        var items = rows.Select(e =>
        {
            User? u = e.UserId != null && userMap.TryGetValue(e.UserId.Value, out var uu) ? uu : null;
            return new EmployeeRow(
                e.Id, e.UserId, e.LastName, e.FirstName, e.MiddleName,
                e.Position, e.Email, e.Phone,
                e.RoleId, e.Role.Name, e.Role.IsSystem,
                e.IsActive, e.FiredAt,
                u is not null, u?.IsActive ?? false, u?.LastLoginAt,
                e.UserId != null && ownerUserId == e.UserId);
        }).ToList();
        return new PagedResult<EmployeeRow>
        { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
    }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult<EmployeeDetail>> Get(Guid orgId, Guid id, CancellationToken ct)
    {
        var dto = await ProjectAsync(orgId, id, ct);
        return dto is null ? NotFound() : Ok(dto);
    }

    [HttpPost]
    public async Task<ActionResult<EmployeeDetail>> Create(Guid orgId, [FromBody] CreateInput input, CancellationToken ct)
    {
        if (!ValidateReason(input.Reason, out var reasonError)) return BadRequest(new { error = reasonError });
        var org = await _db.Organizations.IgnoreQueryFilters().FirstOrDefaultAsync(o => o.Id == orgId, ct);
        if (org is null) return NotFound();
        var role = await _db.EmployeeRoles.IgnoreQueryFilters()
            .FirstOrDefaultAsync(r => r.Id == input.RoleId && r.OrganizationId == orgId, ct);
        if (role is null) return BadRequest(new { error = "Роль не найдена в этой организации." });

        Guid? newUserId = null;
        string? tempPassword = null;
        if (input.CreateAccount)
        {
            var accountEmail = (input.AccountEmail ?? input.Email ?? "").Trim();
            if (string.IsNullOrWhiteSpace(accountEmail))
                return BadRequest(new { error = "Для учётной записи нужен email." });
            var existing = await _userMgr.FindByEmailAsync(accountEmail);
            if (existing is not null)
                return BadRequest(new { error = $"Пользователь с email «{accountEmail}» уже существует." });
            tempPassword = SuperAdminOrganizationsController_Utils.GenerateTempPassword();
            var u = new User
            {
                UserName = accountEmail, Email = accountEmail, EmailConfirmed = true,
                FullName = $"{input.LastName} {input.FirstName}".Trim(),
                OrganizationId = orgId, IsActive = true,
            };
            var ur = await _userMgr.CreateAsync(u, tempPassword);
            if (!ur.Succeeded)
                return BadRequest(new { error = string.Join("; ", ur.Errors.Select(x => x.Description)) });
            // Identity-роль из orgRole (Администратор/Кладовщик/Кассир) — кастомные не получают.
            var identityRole = foodmarket.Api.Infrastructure.IdentityRoleMapper.FromOrgRoleName(role.Name) ?? "Admin";
            await _userMgr.AddToRoleAsync(u, identityRole);
            newUserId = u.Id;
        }

        if (!foodmarket.Application.Common.PhoneNormalization.IsValidOrEmpty(input.Phone))
            return BadRequest(new { error = foodmarket.Application.Common.PhoneNormalization.ErrorMessage });

        var emp = new Employee
        {
            OrganizationId = orgId, UserId = newUserId,
            LastName = input.LastName, FirstName = input.FirstName, MiddleName = input.MiddleName,
            Position = input.Position, Email = input.Email,
            Phone = foodmarket.Application.Common.PhoneNormalization.TryNormalizeKz(input.Phone),
            RoleId = input.RoleId, IsActive = input.IsActive,
        };
        foreach (var rp in input.RetailPointIds ?? [])
            emp.RetailPointAssignments.Add(new EmployeeRetailPointAssignment { OrganizationId = orgId, RetailPointId = rp });
        _db.Employees.Add(emp);
        await _db.SaveChangesAsync(ct);

        await LogAsync("SA_CreateEmployee", orgId,
            $"Создан сотрудник «{emp.LastName} {emp.FirstName}» в «{org.Name}»",
            input.Reason,
            $"{{\"employeeId\":\"{emp.Id}\",\"role\":\"{role.Name}\",\"accountCreated\":{(newUserId != null ? "true" : "false")}}}",
            ct);

        var dto = await ProjectAsync(orgId, emp.Id, ct);
        if (tempPassword is not null)
            return Ok(new { employee = dto, generatedPassword = tempPassword });
        return Ok(new { employee = dto });
    }

    [HttpPut("{id:guid}")]
    public async Task<IActionResult> Update(Guid orgId, Guid id, [FromBody] UpdateInput input, CancellationToken ct)
    {
        if (!ValidateReason(input.Reason, out var reasonError)) return BadRequest(new { error = reasonError });
        var e = await _db.Employees.IgnoreQueryFilters().Include(x => x.RetailPointAssignments)
            .FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == orgId, ct);
        if (e is null) return NotFound();

        var prevRoleId = e.RoleId; var prevActive = e.IsActive;
        e.LastName = input.Employee.LastName;
        e.FirstName = input.Employee.FirstName;
        e.MiddleName = input.Employee.MiddleName;
        e.Position = input.Employee.Position;
        e.Email = input.Employee.Email;
        if (!foodmarket.Application.Common.PhoneNormalization.IsValidOrEmpty(input.Employee.Phone))
            return BadRequest(new { error = foodmarket.Application.Common.PhoneNormalization.ErrorMessage });
        e.Phone = foodmarket.Application.Common.PhoneNormalization.TryNormalizeKz(input.Employee.Phone);
        e.Salary = input.Employee.Salary;
        e.TaxNumber = input.Employee.TaxNumber;
        e.Description = input.Employee.Description;
        e.ImageUrl = input.Employee.ImageUrl;
        e.RoleId = input.Employee.RoleId;
        var nowActive = input.Employee.IsActive;
        if (e.IsActive && !nowActive) e.FiredAt = DateTime.UtcNow;
        if (!e.IsActive && nowActive) e.FiredAt = null;
        e.IsActive = nowActive;

        _db.EmployeeRetailPointAssignments.RemoveRange(e.RetailPointAssignments);
        e.RetailPointAssignments.Clear();
        foreach (var rp in input.Employee.RetailPointIds ?? [])
            e.RetailPointAssignments.Add(new EmployeeRetailPointAssignment { OrganizationId = orgId, RetailPointId = rp });

        await _db.SaveChangesAsync(ct);

        await LogAsync("SA_EditEmployee", orgId,
            $"Изменён сотрудник «{e.LastName} {e.FirstName}»", input.Reason,
            $"{{\"employeeId\":\"{e.Id}\",\"roleChanged\":{(prevRoleId != e.RoleId ? "true" : "false")},\"activeChanged\":{(prevActive != e.IsActive ? "true" : "false")}}}",
            ct);
        return NoContent();
    }

    [HttpPost("{id:guid}/toggle-active")]
    public async Task<IActionResult> ToggleActive(Guid orgId, Guid id, [FromBody] ToggleActiveInput input, CancellationToken ct)
    {
        if (!ValidateReason(input.Reason, out var reasonError)) return BadRequest(new { error = reasonError });
        var e = await _db.Employees.IgnoreQueryFilters()
            .FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == orgId, ct);
        if (e is null) return NotFound();
        if (e.IsActive == input.IsActive) return NoContent();

        e.IsActive = input.IsActive;
        e.FiredAt = input.IsActive ? null : DateTime.UtcNow;
        await _db.SaveChangesAsync(ct);

        await LogAsync(input.IsActive ? "SA_ActivateEmployee" : "SA_DeactivateEmployee", orgId,
            $"{(input.IsActive ? "Активирован" : "Деактивирован")} сотрудник «{e.LastName} {e.FirstName}»",
            input.Reason,
            $"{{\"employeeId\":\"{e.Id}\"}}", ct);
        return NoContent();
    }

    [HttpPost("{id:guid}/account/toggle-active")]
    public async Task<IActionResult> ToggleAccountActive(Guid orgId, Guid id, [FromBody] ToggleAccountActiveInput input, CancellationToken ct)
    {
        if (!ValidateReason(input.Reason, out var reasonError)) return BadRequest(new { error = reasonError });
        var e = await _db.Employees.IgnoreQueryFilters()
            .FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == orgId, ct);
        if (e is null || e.UserId is null) return NotFound();
        var u = await _db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(x => x.Id == e.UserId, ct);
        if (u is null) return NotFound();
        if (u.IsActive == input.IsActive) return NoContent();
        u.IsActive = input.IsActive;
        await _db.SaveChangesAsync(ct);

        if (!input.IsActive)
        {
            // Revoke active OpenIddict tokens чтобы существующие сессии оборвались.
            await _db.Database.ExecuteSqlRawAsync(
                "UPDATE \"OpenIddictTokens\" SET \"Status\" = 'revoked' WHERE \"Status\" = 'valid' AND \"Subject\" = {0}",
                new object[] { u.Id.ToString() });
        }

        await LogAsync(input.IsActive ? "SA_ActivateAccount" : "SA_DeactivateAccount", orgId,
            $"{(input.IsActive ? "Активирована" : "Деактивирована")} учётная запись «{u.Email}»",
            input.Reason,
            $"{{\"employeeId\":\"{e.Id}\",\"userId\":\"{u.Id}\"}}", ct);
        return NoContent();
    }

    [HttpPost("{id:guid}/reset-password")]
    public async Task<ActionResult<ResetPasswordResult>> ResetPassword(Guid orgId, Guid id, [FromBody] ResetPasswordInput input, CancellationToken ct)
    {
        if (!ValidateReason(input.Reason, out var reasonError)) return BadRequest(new { error = reasonError });
        var e = await _db.Employees.IgnoreQueryFilters()
            .FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == orgId, ct);
        if (e is null || e.UserId is null)
            return BadRequest(new { error = "У сотрудника нет учётной записи." });
        var u = await _userMgr.FindByIdAsync(e.UserId.Value.ToString());
        if (u is null) return NotFound();

        var temp = SuperAdminOrganizationsController_Utils.GenerateTempPassword();
        var rm = await _userMgr.RemovePasswordAsync(u);
        if (!rm.Succeeded)
            return BadRequest(new { error = string.Join("; ", rm.Errors.Select(x => x.Description)) });
        var add = await _userMgr.AddPasswordAsync(u, temp);
        if (!add.Succeeded)
            return BadRequest(new { error = string.Join("; ", add.Errors.Select(x => x.Description)) });

        // Revoke все активные tokens — пусть юзер войдёт заново с новым паролем.
        await _db.Database.ExecuteSqlRawAsync(
            "UPDATE \"OpenIddictTokens\" SET \"Status\" = 'revoked' WHERE \"Status\" = 'valid' AND \"Subject\" = {0}",
            new object[] { u.Id.ToString() });

        await LogAsync("SA_ResetPassword", orgId,
            $"Сброшен пароль учётной записи «{u.Email}»", input.Reason,
            $"{{\"employeeId\":\"{e.Id}\",\"userId\":\"{u.Id}\"}}", ct);
        return new ResetPasswordResult(u.Email!, temp);
    }

    [HttpDelete("{id:guid}")]
    public async Task<IActionResult> Delete(Guid orgId, Guid id, [FromQuery] string? reason, CancellationToken ct)
    {
        if (!ValidateReason(reason, out var reasonError)) return BadRequest(new { error = reasonError });
        var e = await _db.Employees.IgnoreQueryFilters()
            .FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == orgId, ct);
        if (e is null) return NotFound();

        var ownerUserId = await _db.Organizations.IgnoreQueryFilters()
            .Where(o => o.Id == orgId).Select(o => o.AccountOwnerUserId).FirstOrDefaultAsync(ct);
        if (e.UserId is not null && ownerUserId == e.UserId)
        {
            // Если удаляем главного администратора — снимаем owner-маркер с орги,
            // иначе он будет указывать в никуда. SuperAdmin обязан до или после
            // назначить нового через PATCH /change-owner.
            var o = await _db.Organizations.IgnoreQueryFilters().FirstAsync(x => x.Id == orgId, ct);
            o.AccountOwnerUserId = null;
        }

        e.IsActive = false;
        e.FiredAt ??= DateTime.UtcNow;
        await _db.SaveChangesAsync(ct);

        await LogAsync("SA_DeleteEmployee", orgId,
            $"Деактивирован сотрудник «{e.LastName} {e.FirstName}»",
            reason,
            $"{{\"employeeId\":\"{e.Id}\"}}", ct);
        return NoContent();
    }

    private async Task<EmployeeDetail?> ProjectAsync(Guid orgId, Guid id, CancellationToken ct)
    {
        var e = await _db.Employees.IgnoreQueryFilters().AsNoTracking()
            .Include(x => x.Role).Include(x => x.RetailPointAssignments)
            .FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == orgId, ct);
        if (e is null) return null;
        var ownerUserId = await _db.Organizations.IgnoreQueryFilters()
            .Where(o => o.Id == orgId).Select(o => o.AccountOwnerUserId).FirstOrDefaultAsync(ct);
        User? u = null;
        if (e.UserId is not null)
            u = await _db.Users.IgnoreQueryFilters().AsNoTracking().FirstOrDefaultAsync(x => x.Id == e.UserId, ct);
        return new EmployeeDetail(
            e.Id, e.OrganizationId, e.UserId,
            e.LastName, e.FirstName, e.MiddleName,
            e.Position, e.Email, e.Phone,
            e.Salary, e.TaxNumber, e.Description, e.ImageUrl,
            e.RoleId, e.Role.Name,
            e.IsActive, e.FiredAt,
            e.RetailPointAssignments.Select(a => a.RetailPointId).ToList(),
            e.UserId != null && ownerUserId == e.UserId,
            u is not null, u?.IsActive ?? false, u?.Email);
    }

    private static bool ValidateReason(string? reason, out string error)
    {
        if (string.IsNullOrWhiteSpace(reason) || reason.Trim().Length < 10)
        {
            error = "Укажите причину (минимум 10 символов) — она пишется в журнал супер-админа.";
            return false;
        }
        error = "";
        return true;
    }

    private async Task LogAsync(string actionType, Guid orgId, string description, string? reason, string changesJson, CancellationToken ct)
    {
        var userIdRaw = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        Guid.TryParse(userIdRaw, out var uid);
        _db.SuperAdminAuditLogs.Add(new SuperAdminAuditLog
        {
            SuperAdminUserId = uid,
            ActionType = actionType, OrganizationId = orgId,
            Description = description, Reason = reason,
            ChangesJson = changesJson,
            IpAddress = HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? "",
        });
        await _db.SaveChangesAsync(ct);
    }
}

internal static class SuperAdminOrganizationsController_Utils
{
    // Дублируем локально, чтобы не выносить статику из соседнего контроллера.
    public static string GenerateTempPassword()
    {
        const string lower = "abcdefghkmnpqrstuvwxyz";
        const string upper = "ABCDEFGHKMNPQRSTUVWXYZ";
        const string digits = "23456789";
        const string special = "!@#$%&*";
        var rnd = new Random();
        var chars = new List<char>
        {
            upper[rnd.Next(upper.Length)],
            lower[rnd.Next(lower.Length)],
            digits[rnd.Next(digits.Length)],
            special[rnd.Next(special.Length)],
        };
        var pool = lower + upper + digits;
        for (var i = 0; i < 8; i++) chars.Add(pool[rnd.Next(pool.Length)]);
        return new string(chars.OrderBy(_ => rnd.Next()).ToArray());
    }
}
