using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using timetracker.Shared;

namespace timetracker.Data;

public class AuthService(
    IDbContextFactory<TimetrackerDbContext> factory, 
    UserNotificationService notifier,
    ITenantProvider tenantProvider) : IAuthService
{
    public async Task<User?> LoginAsync(string username, string password)
    {
        await using var db = await factory.CreateDbContextAsync();
        // Global query filter automatically restricts lookup to current tenant context
        var user = await db.Users
            .FirstOrDefaultAsync(u => u.Username == username);
        if (user == null) return null;
        return VerifyPassword(password, user.PasswordHash, user.PasswordSalt) ? user : null;
    }

    public async Task<List<User>> GetAllUsersAsync()
    {
        await using var db = await factory.CreateDbContextAsync();
        // Superadmin bypasses query filters to manage all users globally
        return await db.Users
            .IgnoreQueryFilters()
            .OrderBy(u => u.Username)
            .ToListAsync();
    }

    public async Task DeleteUserAsync(int userId)
    {
        await using var db = await factory.CreateDbContextAsync();
        var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
        if (user != null)
        {
            db.Users.Remove(user);
            await db.SaveChangesAsync();
            await notifier.NotifyUserDeletedAsync(userId);
            await notifier.NotifyUsersChangedAsync();
        }
    }

    public async Task<string?> RenameUserAsync(int userId, string newUsername)
    {
        if (string.IsNullOrWhiteSpace(newUsername) || newUsername.Length < 3)
            return "Benutzername muss mindestens 3 Zeichen lang sein.";

        await using var db = await factory.CreateDbContextAsync();
        
        // Find user ignoring filters so we can rename global admin too if needed
        var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null) return "Benutzer nicht gefunden.";

        // Check if name is taken within the same tenant context
        var isTaken = await db.Users.AnyAsync(u => u.Username == newUsername && u.Id != userId && u.TenantId == user.TenantId);
        if (isTaken)
            return "Benutzername in dieser Firma bereits vergeben.";

        user.Username = newUsername;
        await db.SaveChangesAsync();
        await notifier.NotifyUsersChangedAsync();
        return null;
    }

    public async Task<string?> ChangePasswordAsync(int userId, string currentPassword, string newPassword)
    {
        if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
            return "Passwort muss mindestens 6 Zeichen lang sein.";

        await using var db = await factory.CreateDbContextAsync();
        var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null) return "Benutzer nicht gefunden.";

        if (!VerifyPassword(currentPassword, user.PasswordHash, user.PasswordSalt))
            return "Das aktuelle Passwort ist nicht korrekt.";

        var (hash, salt) = HashPassword(newPassword);
        user.PasswordHash = hash;
        user.PasswordSalt = salt;

        await db.SaveChangesAsync();
        return null;
    }

    public async Task<(User? User, string? Error)> RegisterAsync(string username, string password, string? honeypot = null)
    {
        if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
            return (null, "Benutzername muss mindestens 3 Zeichen lang sein.");
        if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
            return (null, "Passwort muss mindestens 6 Zeichen lang sein.");

        var tenantId = tenantProvider.TenantId;
        if (tenantId == null && username.Equals("marc", StringComparison.OrdinalIgnoreCase))
        {
            return (null, "Dieser Benutzername ist reserviert.");
        }

        await using var db = await factory.CreateDbContextAsync();
        // Filter handles checking uniqueness within the same tenant context
        if (await db.Users.AnyAsync(u => u.Username == username))
            return (null, tenantId.HasValue ? "Benutzername in dieser Firma bereits vergeben." : "Benutzername bereits vergeben.");

        var (hash, salt) = HashPassword(password);
        var user = new User 
        { 
            Username = username, 
            PasswordHash = hash, 
            PasswordSalt = salt,
            TenantId = tenantId
        };
        db.Users.Add(user);
        await db.SaveChangesAsync();
        await notifier.NotifyUsersChangedAsync();
        return (user, null);
    }

    public async Task<(Tenant? Tenant, string? Error)> RegisterFirmAsync(RegisterFirmRequest req)
    {
        if (string.IsNullOrWhiteSpace(req.CompanyName) || req.CompanyName.Length < 3)
            return (null, "Firmenname muss mindestens 3 Zeichen lang sein.");
        if (string.IsNullOrWhiteSpace(req.Subdomain) || req.Subdomain.Length < 3)
            return (null, "Subdomain muss mindestens 3 Zeichen lang sein.");
        if (string.IsNullOrWhiteSpace(req.AdminUsername) || req.AdminUsername.Length < 3)
            return (null, "Administrator-Benutzername muss mindestens 3 Zeichen lang sein.");
        if (string.IsNullOrWhiteSpace(req.AdminPassword) || req.AdminPassword.Length < 6)
            return (null, "Passwort muss mindestens 6 Zeichen lang sein.");

        var subdomain = req.Subdomain.ToLower().Trim();
        if (!subdomain.All(c => char.IsLetterOrDigit(c) || c == '-'))
        {
            return (null, "Subdomain darf nur Buchstaben, Zahlen und Bindestriche enthalten.");
        }

        var reservedSubdomains = new[] { "www", "api", "admin", "superadmin", "localhost", "mail", "portal", "app", "dev", "test" };
        if (reservedSubdomains.Contains(subdomain))
        {
            return (null, "Diese Subdomain ist reserviert und kann nicht verwendet werden.");
        }

        await using var db = await factory.CreateDbContextAsync();
        
        // Tenant checks are global, no filters applied to Tenants table
        var exists = await db.Tenants.AnyAsync(t => t.Subdomain == subdomain);
        if (exists)
        {
            return (null, "Diese Subdomain ist bereits vergeben.");
        }

        await using var transaction = await db.Database.BeginTransactionAsync();
        try
        {
            var tenant = new Tenant
            {
                Name = req.CompanyName,
                Subdomain = subdomain,
                IsApproved = false,
                CreatedAt = DateTime.UtcNow
            };
            db.Tenants.Add(tenant);
            await db.SaveChangesAsync();

            var (hash, salt) = HashPassword(req.AdminPassword);
            var user = new User
            {
                Username = req.AdminUsername,
                PasswordHash = hash,
                PasswordSalt = salt,
                TenantId = tenant.Id,
                IsTenantAdmin = true
            };
            db.Users.Add(user);
            await db.SaveChangesAsync();

            await transaction.CommitAsync();
            
            await notifier.NotifyUsersChangedAsync();
            return (tenant, null);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
            return (null, $"Registrierungsfehler: {ex.Message}");
        }
    }

    public async Task<string?> AssignTenantAsync(int userId, int? tenantId)
    {
        await using var db = await factory.CreateDbContextAsync();
        
        var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null) return "Benutzer nicht gefunden.";

        if (tenantId.HasValue)
        {
            var tenantExists = await db.Tenants.AnyAsync(t => t.Id == tenantId.Value);
            if (!tenantExists) return "Firma existiert nicht.";
        }

        await using var transaction = await db.Database.BeginTransactionAsync();
        try
        {
            user.TenantId = tenantId;

            var settings = await db.AppSettings.IgnoreQueryFilters().Where(s => s.UserId == userId).ToListAsync();
            foreach (var s in settings) s.TenantId = tenantId;

            var workDays = await db.WorkDays.IgnoreQueryFilters().Where(w => w.UserId == userId).ToListAsync();
            foreach (var w in workDays) w.TenantId = tenantId;

            var vacationDays = await db.VacationDays.IgnoreQueryFilters().Where(v => v.UserId == userId).ToListAsync();
            foreach (var v in vacationDays) v.TenantId = tenantId;

            await db.SaveChangesAsync();
            await transaction.CommitAsync();

            await notifier.NotifyUsersChangedAsync();
            return null;
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
            return $"Fehler beim Verschieben des Benutzers: {ex.Message}";
        }
    }

    public async Task<string?> ResetPasswordAsync(int userId, string newPassword)
    {
        if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
            return "Passwort muss mindestens 6 Zeichen lang sein.";

        await using var db = await factory.CreateDbContextAsync();
        var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null) return "Benutzer nicht gefunden.";

        var (hash, salt) = HashPassword(newPassword);
        user.PasswordHash = hash;
        user.PasswordSalt = salt;

        await db.SaveChangesAsync();
        return null;
    }

    private static (string hash, string salt) HashPassword(string password)
    {
        var saltBytes = RandomNumberGenerator.GetBytes(32);
        var salt = Convert.ToBase64String(saltBytes);
        var hash = ComputeHash(password, salt);
        return (hash, salt);
    }

    private static bool VerifyPassword(string password, string hash, string salt)
        => ComputeHash(password, salt) == hash;

    private static string ComputeHash(string password, string salt)
    {
        var hash = Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(password),
            Convert.FromBase64String(salt),
            iterations: 200_000,
            HashAlgorithmName.SHA256,
            outputLength: 32);
        return Convert.ToBase64String(hash);
    }
}
