using Cysharp.Threading.Tasks;
using ShrinkNetwork;

namespace ShrinkNetwork.ServerHost.Framework;

public sealed class ShrinkDedicatedServerApp
{
    private readonly List<IShrinkServerModule> _modules;
    private readonly ServerModuleContext _context;

    public ShrinkDedicatedServerApp(ServerHostOptions options, params IShrinkServerModule[] modules)
    {
        _modules = modules.ToList();
        _context = new ServerModuleContext(options, new ServerAuthStore());
    }

    public ServerModuleContext Context => _context;

    public async UniTask StartAsync(CancellationToken cancellationToken = default)
    {
        if (_context.Options.EnableUnityCodeScan)
        {
            var manifest = UnityNetworkCodeScanner.Scan(_context.Options.UnityAssetsPath);
            UnityNetworkCodeScanner.WriteOutputs(manifest, _context.Options.ScanOutputDirectory);
            _context.ScanManifest = manifest;
        }

        var tcpService = CreateService("TCP");
        var kcpService = CreateService("KCP");

        _context.RegisterService("TCP", tcpService);
        _context.RegisterService("KCP", kcpService);

        foreach (var module in _modules)
        {
            module.ConfigureService(_context, tcpService, "TCP");
            module.ConfigureService(_context, kcpService, "KCP");
        }

        tcpService.BindTransport(new TcpServerTransport(System.Net.IPAddress.Any, _context.Options.Port,
            tlsOptions: BuildTcpTlsOptions()));
        kcpService.BindTransport(new KcpServerTransport(System.Net.IPAddress.Any, _context.Options.Port, new ShrinkKcpTransportOptions
        {
            Interval = 10,
            UpdateIntervalMs = 10,
            IdleTimeoutMs = 15000,
            HandshakeRetryMs = 250,
            ConnectTimeoutMs = 4000
        }));

        foreach (var module in _modules)
            module.StartAsync(_context, cancellationToken).Forget();

        await UniTask.CompletedTask;
    }

    private ShrinkNetworkService CreateService(string transportName)
    {
        var service = new ShrinkNetworkService(new ShrinkJsonNetworkSerializer(),
            new ShrinkNetworkMessageRegistry(),
            new ShrinkNetworkRouter());
        service.MinProtocolVersion = _context.Options.MinProtocolVersion;
        service.MaxProtocolVersion = _context.Options.MaxProtocolVersion;
        service.MinSchemaVersion = _context.Options.MinSchemaVersion;
        service.MaxSchemaVersion = _context.Options.MaxSchemaVersion;
        service.DisconnectOnProtocolViolation = _context.Options.DisconnectOnProtocolViolation;
        service.IncomingPacketValidator = (session, packet) => ValidateIncomingPacket(service, session, packet);

        service.OnSessionConnected += session =>
        {
            session.SetPeerKind(ShrinkNetworkPeerKind.Client);
            ShrinkNetworkLogger.Info($"[{transportName}] Client connected: {session.SessionId} {session.RemoteAddress}");
        };
        service.OnSessionDisconnected += session =>
        {
            _context.AuthStore.Remove(service, session.SessionId);
            ShrinkNetworkLogger.Info($"[{transportName}] Client disconnected: {session.SessionId}");
        };

        return service;
    }

    private ShrinkIncomingPacketValidationResult ValidateIncomingPacket(
        ShrinkNetworkService service,
        ShrinkNetworkSession session,
        ShrinkNetworkPacket packet)
    {
        if (!_context.Options.EnableSessionTokens)
            return ShrinkIncomingPacketValidationResult.Allow;
        if (packet.Kind == ShrinkNetworkPacketKind.Response)
            return ShrinkIncomingPacketValidationResult.Allow;
        if (string.Equals(packet.Route, AuthServerModule.LoginRoute, StringComparison.Ordinal) ||
            string.Equals(packet.Route, AuthServerModule.RefreshRoute, StringComparison.Ordinal))
            return ShrinkIncomingPacketValidationResult.Allow;
        if (!_context.AuthStore.TryGetState(service, session.SessionId, out _))
            return ShrinkIncomingPacketValidationResult.Allow;

        if (_context.AuthStore.TryValidateSessionToken(service, session.SessionId, packet.SessionToken, out var authState,
                out var failureReason))
        {
            session.SetSessionToken(authState.SessionToken, authState.SessionTokenExpiresAtUtc);
            return ShrinkIncomingPacketValidationResult.Allow;
        }

        return ShrinkIncomingPacketValidationResult.Reject(failureReason,
            _context.Options.DisconnectOnInvalidSessionToken);
    }

    private ShrinkTcpTlsOptions? BuildTcpTlsOptions()
    {
        if (!_context.Options.EnableTcpTls)
            return null;

        return new ShrinkTcpTlsOptions
        {
            Enabled = true,
            TargetHost = _context.Options.TcpTlsTargetHost,
            ServerCertificatePath = _context.Options.TcpTlsCertificatePath,
            ServerCertificatePassword = _context.Options.TcpTlsCertificatePassword,
            CheckCertificateRevocation = _context.Options.TcpTlsCheckCertificateRevocation
        };
    }
}
