using System.Text.RegularExpressions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Users;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Managing.Application.Users;

public class UserService : IUserService
{
    private readonly IEvmManager _evmManager;
    private readonly IUserRepository _userRepository;
    private readonly IAccountService _accountService;
    private readonly ILogger<UserService> _logger;
    private readonly ICacheService _cacheService;
    private readonly IGrainFactory _grainFactory;
    private readonly string[] _authorizedAddresses;

    public UserService(
        IEvmManager evmManager,
        IUserRepository userRepository,
        IAccountService accountService,
        ILogger<UserService> logger,
        ICacheService cacheService,
        IGrainFactory grainFactory,
        IConfiguration configuration)
    {
        _evmManager = evmManager;
        _userRepository = userRepository;
        _accountService = accountService;
        _logger = logger;
        _cacheService = cacheService;
        _grainFactory = grainFactory;

        var authorizedAddressesString = configuration["AUTHORIZED_ADDRESSES"] ?? string.Empty;
        _authorizedAddresses = string.IsNullOrEmpty(authorizedAddressesString)
            ? Array.Empty<string>()
            : authorizedAddressesString.Split(';', StringSplitOptions.RemoveEmptyEntries);
    }

    public async Task<User> Authenticate(string name, string address, string message, string signature)
    {
        var recoveredAddress = _evmManager.VerifySignature(signature, message);

        // Verify message 
        if (!message.Equals("KaigenTeamXCowchain"))
        {
            _logger.LogWarning($"Message {message} not starting with KaigenTeamXCowchain");
            throw new Exception(
                $"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}");
        }

        if (!_authorizedAddresses.Any(a => string.Equals(a, recoveredAddress, StringComparison.OrdinalIgnoreCase)))
        {
            throw new Exception($"Address {recoveredAddress}  not authorized");
        }

        if (recoveredAddress == null || !recoveredAddress.Equals(address))
        {
            throw new Exception($"Address {recoveredAddress} not corresponding");
        }

        // Check if account exist
        var user = await _userRepository.GetUserByNameAsync(name);

        if (user != null)
        {
            if (!user.Name.Equals(name))
                throw new Exception("Name not corresponding to user " + name);

            // User and account found
            user.Accounts = _accountService.GetAccountsByUser(user).ToList();

            // Check if recoverred address owns the account
            var account = user.Accounts.FirstOrDefault(a => a.Key.Equals(recoveredAddress));

            if (account == null)
            {
                throw new Exception("Account not found for address " + recoveredAddress);
            }

            return user;
        }
        else
        {
            // First login - create user first
            user = new User
            {
                Name = name
            };

            // Save the user first
            await _userRepository.SaveOrUpdateUserAsync(user);

            // Create embedded account to authenticate user after user is saved
            var account = await _accountService.CreateAccount(user, new Account
            {
                Name = $"{name}-embedded",
                Key = recoveredAddress,
                Exchange = Enums.TradingExchanges.Evm,
                Type = Enums.AccountType.Privy
            });

            user.Accounts = new List<Account>()
            {
                account
            };
        }

        return user;
    }

    public async Task<User> GetUserByAddressAsync(string address, bool useCache = true)
    {
        var cacheKey = $"user-by-address-{address}";

        // Check cache first if caching is enabled
        if (useCache)
        {
            var cachedUser = _cacheService.GetValue<User>(cacheKey);
            if (cachedUser != null)
            {
                return cachedUser;
            }
        }

        // Fetch from database (either cache miss or cache disabled)
        var account = await _accountService.GetAccountByKey(address, true, false);
        var user = await _userRepository.GetUserByNameAsync(account.User.Name);

        // Use proper async version to avoid DbContext concurrency issues
        user.Accounts = (await _accountService.GetAccountsByUserAsync(user)).ToList();

        // Save to cache for 10 minutes if caching is enabled (JWT middleware calls this on every request)
        if (useCache)
        {
            _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10));
        }

        return user;
    }

    public async Task<User> UpdateAgentName(User user, string agentName)
    {
        // Check if agent name is already used
        var existingUser = await _userRepository.GetUserByAgentNameAsync(agentName);
        if (existingUser != null)
        {
            throw new Exception($"Agent name already used by {existingUser.Name}");
        }
        else
        {
            user = await GetUserByName(user.Name);
            if (!string.IsNullOrEmpty(user.AgentName) && user.AgentName.Equals(agentName))
                return user;

            // Update the agent name on the provided user object
            user.AgentName = agentName;
            await _userRepository.SaveOrUpdateUserAsync(user);

            // Initialize the AgentGrain for this user
            try
            {
                var agentGrain = _grainFactory.GetGrain<IAgentGrain>(user.Id);
                await agentGrain.InitializeAsync(user.Id, agentName);
                _logger.LogInformation("AgentGrain initialized for user {UserId} with agent name {AgentName}", user.Id,
                    agentName);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to initialize AgentGrain for user {UserId} with agent name {AgentName}",
                    user.Id, agentName);
                // Don't throw here to avoid breaking the user update process
            }

            return user;
        }
    }

    public async Task<User> UpdateAvatarUrl(User user, string avatarUrl)
    {
        // Validate URL format and image extension
        if (!Uri.TryCreate(avatarUrl, UriKind.Absolute, out Uri? uriResult) ||
            (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
        {
            throw new Exception("Invalid URL format");
        }

        // Check for valid image extension
        string pattern = @"\.(jpeg|jpg|png)$";
        if (!Regex.IsMatch(avatarUrl, pattern, RegexOptions.IgnoreCase))
        {
            throw new Exception("URL must point to a JPEG or PNG image");
        }

        user = await GetUserByName(user.Name);
        if (!string.IsNullOrEmpty(user.AvatarUrl) && user.AvatarUrl.Equals(avatarUrl))
            return user;

        // Update the avatar URL on the provided user object
        user.AvatarUrl = avatarUrl;
        await _userRepository.SaveOrUpdateUserAsync(user);
        return user;
    }

    public async Task<User> UpdateTelegramChannel(User user, string telegramChannel)
    {
        // Validate Telegram channel format (numeric channel ID only)
        if (!string.IsNullOrEmpty(telegramChannel))
        {
            string pattern = @"^[0-9]{5,15}$";
            if (!Regex.IsMatch(telegramChannel, pattern))
            {
                throw new Exception("Invalid Telegram channel format. Must be numeric channel ID (5-15 digits).");
            }
        }

        user = await GetUserByName(user.Name);
        if (!string.IsNullOrEmpty(user.TelegramChannel) && user.TelegramChannel.Equals(telegramChannel))
            return user;

        // Update the telegram channel on the provided user object
        user.TelegramChannel = telegramChannel;
        await _userRepository.SaveOrUpdateUserAsync(user);
        return user;
    }

    public async Task<User> GetUserByName(string name)
    {
        return await _userRepository.GetUserByNameAsync(name);
    }

    public async Task<User> GetUserByAgentName(string agentName)
    {
        var user = await _userRepository.GetUserByAgentNameAsync(agentName);
        if (user == null)
        {
            throw new Exception($"User with agent name {agentName} not found");
        }

        return user;
    }

    public async Task<IEnumerable<User>> GetAllUsersAsync()
    {
        return await _userRepository.GetAllUsersAsync();
    }

    public async Task<User> GetUserByIdAsync(int userId)
    {
        var allUsers = await _userRepository.GetAllUsersAsync();
        var user = allUsers.FirstOrDefault(u => u.Id == userId);

        if (user == null)
        {
            throw new Exception($"User with ID {userId} not found");
        }

        return user;
    }
}