/* 
 *                                dbMango
 *
 * Copyright 2025 Deutsche Bank AG
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
﻿using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Rms.Risk.Mango.Interfaces;
using Rms.Risk.Mango.Pivot.Core;
using Rms.Risk.Mango.Pivot.Core.MongoDb;
using Rms.Risk.Mango.Services.Context;
using Rms.Risk.Mango.Services.Security;
using Rms.Service.Bootstrap.Security;

namespace Rms.Risk.Mango.Services;

internal class UserSession : IUserSession
{
    public UserService User => _user;

    public string? TaskNumber     { get; set; }
    public string? TaskCheckError { get; private set; }

    private          CheckerReply?                         _checkReply;
    private readonly UserService                           _user;
    private readonly IMongoDbServiceFactory                _mongoDbServiceFactory;
    private readonly IChangeNumberChecker                  _changeNumberChecker;
    private readonly IDatabaseConfigurationService         _databases;

    public UserSession(IOptions<DbMangoSettings>             settings, 
                       UserService                           user, 
                       IMongoDbServiceFactory                mongoDbServiceFactory, 
                       IChangeNumberChecker                  changeNumberChecker,
                       IDatabaseConfigurationService         databases)
    {
        _user                  = user;
        _mongoDbServiceFactory = mongoDbServiceFactory;
        _changeNumberChecker   = changeNumberChecker;
        _databases             = databases;
        
        Database               = settings.Value.Initial;
        DatabaseInstance       = _databases.Databases[settings.Value.Initial].Config.MongoDbDatabase;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not IUserSession other)
            return false;

        return string.Equals(_user.GetEmail(), other.User.GetEmail(),  StringComparison.Ordinal) &&
               string.Equals(Database,         other.Database,         StringComparison.Ordinal) &&
               string.Equals(DatabaseInstance, other.DatabaseInstance, StringComparison.Ordinal) &&
               string.Equals(TaskNumber,       other.TaskNumber,       StringComparison.Ordinal);
    }

    // ReSharper disable NonReadonlyMemberInGetHashCode
    public override int GetHashCode() => HashCode.Combine(_user.GetEmail(), Database, DatabaseInstance, TaskNumber);
    // ReSharper restore NonReadonlyMemberInGetHashCode

    public async Task<bool> HasValidTask()
    {
        TaskNumber     ??= "ITSK0000000000";
        TaskCheckError =   null;

        if (string.IsNullOrWhiteSpace(TaskNumber) || string.IsNullOrWhiteSpace(User.GetEmail()))
        {
            TaskCheckError = "Task number or user email are not set.";
            return false;
        }

        var now = DateTime.UtcNow;

        if (_checkReply == null)
        {
            _checkReply = await _changeNumberChecker.IsValid(TaskNumber, User.GetEmail(), now);
            if (!_checkReply.IsValid)
            {
                TaskCheckError = _checkReply.ErrorMessage;
                TaskNumber = null;
                _checkReply = null;
                return false;
            }
            TaskCheckError = null;
        }
        
        if ( _checkReply == null )
        {
            TaskCheckError = "Task check reply is null.";
            return false;
        }

        var isOpen = DateTime.UtcNow > _checkReply.ValidFromUtc 
            && DateTime.UtcNow < _checkReply.ValidToUtc;

        if ( !isOpen)
        {
            TaskCheckError = $"Task {TaskNumber} is valid, but implementation window is not open (Start: {_checkReply.ValidFromUtc}, End: {_checkReply.ValidToUtc} UTC)";
            return false;
        }
        
        return true;
    }

    public async Task<bool> CanAccess(IAuthorizationService auth, string policyName, string databaseName)
    {
        var res = await auth.AuthorizeAsync(User.GetUser(), databaseName,
        [
            policyName switch
            {
                DatabaseAccessPolicyExtensions.ReadAccessPolicy => new ReadAccessRequirement(),
                DatabaseAccessPolicyExtensions.WriteAccessPolicy => new WriteAccessRequirement(),
                DatabaseAccessPolicyExtensions.AdminAccessPolicy => new AdminAccessRequirement(),
                _ => throw new ($"Policy name \"{policyName}\" is invalid. Expecting {DatabaseAccessPolicyExtensions.ReadAccessPolicy}, {DatabaseAccessPolicyExtensions.WriteAccessPolicy}, or {DatabaseAccessPolicyExtensions.AdminAccessPolicy}")
            }
        ]);

        return res.Succeeded;
    }

    private Dictionary<string, DatabasesConfig.DatabaseConfig> Databases => _databases.Databases;

    public string Database
    {
        get;
        set
        {
            if (field == value)
                return;

            Clear();
            field = value;

            var config = GetDatabaseConfig(Database);

            // this is to make sure that DatabaseChanged is only called once
            var newInstance = IsDatabaseInstanceSelectionAllowed
                    ? ""
                    : config.Config.MongoDbDatabase
                ;

            if (DatabaseInstance != newInstance)
                DatabaseInstance = newInstance;
            else
                DatabaseChanged?.Invoke();
        }
    }

    public bool IsDatabaseInstanceSelectionAllowed => IsInstanceSelectionAllowed(Database);

    public bool IsInstanceSelectionAllowed(string database)
    {
        if (string.IsNullOrWhiteSpace(database))
            return false;

        if (Databases.TryGetValue(database, out var config))
        {
            return string.IsNullOrWhiteSpace(config.Config.MongoDbDatabase);
        }

        return false;
    }

    private void Clear()
    {
    }

    public event Action? DatabaseChanged;

    public string Collection { get; set; } = "";

    public string DatabaseInstance
    {
        get
        {
            if ( !string.IsNullOrWhiteSpace(field) )
                return field;

            if (!string.IsNullOrWhiteSpace(Database)) 
                return "";

            var config = GetDatabaseConfig(Database);

            // this is to make sure that DatabaseChanged is only called once
            if ( !IsDatabaseInstanceSelectionAllowed )
                field = config.Config.MongoDbDatabase;

            return field;
        }
        set
        {
            if (field == value)
                return;
            Clear();
            field = string.IsNullOrWhiteSpace(value) ? "" : value.Trim();
            DatabaseChanged?.Invoke();
        }
    }

    public IMongoDbService<BsonDocument> MongoDb
    {
        get
        {
            if (string.IsNullOrWhiteSpace(Collection))
                throw new("Collection is not selected");

            _ = GetDatabaseConfig(Database);

            return _mongoDbServiceFactory.Create(Database, Collection, DatabaseInstance);
        }
    }

    public IMongoDbDatabaseAdminService MongoDbAdmin => GetCustomAdmin(Database, DatabaseInstance);

    public IMongoDbDatabaseAdminService GetCustomAdmin(string databaseName, string databaseInstance)
    {
        _ = GetDatabaseConfig(Database);
        return _mongoDbServiceFactory.CreateAdmin(databaseName, this, databaseInstance);
    }

    public IMongoDbDatabaseAdminService MongoDbAdminForAdminDatabase
    {
        get
        {
            _ = GetDatabaseConfig(Database);
            return _mongoDbServiceFactory.CreateAdmin(Database, this, "admin");
        }
    }

    public IPivotTableDataSource PivotDataSource
    {
        get
        {
            _ = GetDatabaseConfig(Database);
            return _mongoDbServiceFactory.CreatePivot(Database, DatabaseInstance);
        }
    }

    public IAuditService Audit
    {
        get
        {
            _ = GetDatabaseConfig(Database);
            return _mongoDbServiceFactory.CreateAudit(Database, DatabaseInstance);
        }
    }

    public IMongoDbDatabaseAdminService GetShardConnection(
        string  host, 
        int     port
        )
    {
        var config = GetDatabaseConfig(Database);

        var shardConfig = config.Config.Clone();
        shardConfig.DirectConnection = true;
        shardConfig.AllowShardAccess = false;
        shardConfig.MongoDbUrl       = $"mongodb://{host}:{port}";

        var db = _mongoDbServiceFactory.CreateAdmin(shardConfig, Database, this, DatabaseInstance);
        return db;
    }

    public MongoDbConfigRecord DatabaseConfig => GetDatabaseConfig(Database).Config;

    private DatabasesConfig.DatabaseConfig GetDatabaseConfig(string database)
    {
        if (!Databases.TryGetValue(database, out var cfg))
            throw new($"Database=\"{database}\" is not configured");
        return cfg;
    }

    public DatabasesConfig.DatabaseConfig.LdapGroups LdapGroups
    {
        get
        {
            if (!Databases.TryGetValue(Database, out var cfg))
                throw new($"Database=\"{Database}\" is not configured");
            return cfg.Groups;
        }
    }
}