using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

using CommonHelpers;

using MyFlat.DB;

namespace MyFlat {

//**************************************************************************************************
/// <summary>
/// Instance class of application, considering current HTTP-request (in context of user session)
/// </summary>
class App {
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

public ControllerBase Controller { get; private set; }
public ISession Session => Controller.HttpContext.Session;

public bool IsUserSessionOpen =>
   GetSessionData() != null;

//==================================================================================================

/// <exception cref="InvalidOperationException" />
public SessionData SessionData
{
   get
   {
      var sd = GetSessionData();
      if ( sd == null )
         throw SessionAccessError ??
            new InvalidOperationException(HLocal.TEXT(
               "User session data is not available.",
               "Данные пользовательского сеанса недоступны." ));
      return sd;
   }
}
[HOptionalValue]
public InvalidOperationException? SessionAccessError =>
   IsUserSessionOpen ? null :
      new InvalidOperationException(
         _ExpiredUserSessionId_ != Guid.Empty ?
            string.Format(HLocal.TEXT(
               "User session has expired: \"{0}\".",
               "Пользовательский сеанс истёк: \"{0}\"." ),
               _ExpiredUserSessionId_ ) :
         HLocal.TEXT(
            "User session is not open.",
            "Вход пользователя не был произведён." ) );

/// <exception cref="InvalidOperationException" />
public string LoginName => SessionData.LoginName;
/// <exception cref="InvalidOperationException" />
public long AccountCode => SessionData.AccountCode;
/// <exception cref="InvalidOperationException" />
public string UserName => SessionData.UserName;
/// <exception cref="InvalidOperationException" />
public DateTime LoginTime => SessionData.LoginTime;
/// <exception cref="InvalidOperationException" />
public Guid UserSessionId => SessionData.Id;

/// <exception cref="ArgumentNullException" />
public App(ControllerBase controller)
{
   HArgChecking.VerifyNotNull( controller );
   Controller = controller;
   byte[] guidBytes;
   if ( Session.TryGetValue( _UserSessionIdKey_, out guidBytes ) )
   {
      var sd = _SessionDataDictionary_[ new Guid( guidBytes ) ];
      if ( Session.TryGetValue( _UserSessionControlIdKey_, out guidBytes ) &&
           sd.ControlId == new Guid( guidBytes ) )
         if ( IsUserSessionExpired( sd ) )
            RemoveSessionData();
         else
         {
            sd.SetLastRequestTime( DateTime.Now );
            _SessionData_ = sd;
         }
   }
}

/// <exception cref="InvalidOperationException" />
public void VerifyUserSessionOpen()
{
   var error = SessionAccessError;
   if ( error != null )
      throw error;
}

/// <exception cref="ArgumentNullException" />
public void CreateUserSession( Guid accountId, long accountCode,
                               AccountInfo_Item accountProps,
                               AuthorizationInfo ai )
{
   var controlId = Guid.NewGuid();
   var sd = new SessionData( accountId, accountCode, accountProps, ai, controlId );
   _SessionDataDictionary_[ sd.Id ] = sd;
   Session.Set( _UserSessionIdKey_, sd.Id.ToByteArray() );
   Session.Set( _UserSessionControlIdKey_, controlId.ToByteArray() );
}

public SessionData? GetSessionData() =>
   _SessionData_;

public void RemoveSessionData()
{
   byte[] guidBytes;
   if ( Session.TryGetValue( _UserSessionIdKey_, out guidBytes ) )
   {
      SessionData? sd;
      _SessionDataDictionary_.TryRemove( new Guid(guidBytes), out sd );
   }
   Session.Remove( _UserSessionIdKey_ );
   Session.Remove( _UserSessionControlIdKey_ );
}

public void RemoveOldSessions()
{
   List<Guid> idList;
   lock ( _SessionDataDictionary_ )
      idList = new List<Guid>( _SessionDataDictionary_.Keys );
   foreach (Guid id in idList)
   {
      SessionData? sd;
      if ( _SessionDataDictionary_.TryGetValue( id, out sd ) &&
           IsUserSessionExpired( sd ) )
         _SessionDataDictionary_.TryRemove( id, out sd );
   }
}

/// <exception cref="InvalidOperationException" />
public T? RestoreModel<T>() where T: class,ICloneable =>
   (T?)SessionData.GetValueOrDefault( typeof(T) );
/// <exception cref="ArgumentNullException" />
/// <exception cref="InvalidOperationException" />
public void SaveModel<T>(ICloneable model)
{
   HArgChecking.VerifyNotNull( model );
   SessionData[ typeof(T) ] = model;
}
/// <exception cref="InvalidOperationException" />
public void ResetModel<T>() =>
   SessionData[ typeof(T) ] = null;
/// <exception cref="InvalidOperationException" />
public void ClearModels() =>
   SessionData.ClearDictionary();

static bool IsUserSessionExpired(SessionData sd)
{
   DateTime now = DateTime.Now;
   return
      now - sd.LoginTime > _UserSessionExpirationPeriod_ ||
      now - sd.LastRequestTime > _MaxInterrequestPeriod_;
}

//==================================================================================================

static readonly string
   _UserSessionIdKey_ = Guid.NewGuid().ToString(), // instead of "UserSessionId" (e.g.)
   _UserSessionControlIdKey_ = Guid.NewGuid().ToString(); // instead of "UserSessionControlId"
static readonly TimeSpan
   _UserSessionExpirationPeriod_ = new TimeSpan( 1, 0, 0 ), // one hour
   _MaxInterrequestPeriod_ = new TimeSpan( 0, 10, 0 ); // ten minutes
static readonly ConcurrentDictionary<Guid,SessionData>
   _SessionDataDictionary_ = new ConcurrentDictionary<Guid,SessionData>();

//--------------------------------------------------------------------------------------------------

readonly SessionData? _SessionData_;

//--------------------------------------------------------------------------------------------------

Guid _ExpiredUserSessionId_ = Guid.Empty;

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
} // App
//**************************************************************************************************

} // MyFlat
