using System.Runtime.InteropServices;
using System.Threading.Channels;

// The public, safe C# surface over libvoicecat. Everything below is a thin wrapper around
// NativeMethods — see docs/architecture.md §4 ("the core owns audio; C# only orchestrates").
namespace VoiceCat.Interop;

public sealed class VoiceCatClient : IDisposable
{
    private readonly VoiceCatClientHandle _handle = new();
    private readonly GCHandle _selfHandle;

    // Native string buffers backing vc_config — must outlive the WHOLE client lifetime, not
    // just vc_client_create(): client_name/client_version are read later, whenever connect()
    // actually runs on io_thread_ (vc_client just stores the raw pointers from vc_config by
    // value, it does not copy the string data). Freed in Dispose(), after vc_client_destroy
    // has returned (which synchronously joins every internal thread, so nothing can still be
    // reading these pointers by then).
    private nint _clientNamePtr;
    private nint _clientVersionPtr;
    private nint _tofuStorePathPtr;

    private readonly Channel<VoiceCatEvent> _events =
        Channel.CreateUnbounded<VoiceCatEvent>(new UnboundedChannelOptions
        {
            SingleReader = true,
            SingleWriter = true,
        });

    // on_level fires far more often than on_event and intermediate values are visually
    // irrelevant — coalesce to "latest sample per stream_id" instead of queuing every one.
    private readonly System.Collections.Concurrent.ConcurrentDictionary<uint, float> _latestLevels = new();

    /// <summary>Raised from PumpEvents() (i.e. on whatever thread calls it — see that method's
    /// doc comment) for every event, in order, never coalesced.</summary>
    public event Action<VoiceCatEvent>? EventReceived;

    /// <summary>Raised from PumpEvents() with the latest RMS level per stream_id since the
    /// last pump.</summary>
    public event Action<uint, float>? LevelChanged;

    public unsafe VoiceCatClient(string clientName, string clientVersion,
        VcLogLevel logLevel = VcLogLevel.Info, string? tofuStorePath = null)
    {
        _selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);

        _clientNamePtr = Marshal.StringToCoTaskMemUTF8(clientName);
        _clientVersionPtr = Marshal.StringToCoTaskMemUTF8(clientVersion);
        _tofuStorePathPtr = tofuStorePath is null ? 0 : Marshal.StringToCoTaskMemUTF8(tofuStorePath);

        var cfg = new VcConfigNative
        {
            ClientName = _clientNamePtr,
            ClientVersion = _clientVersionPtr,
            LogLevel = logLevel,
            TofuStorePath = _tofuStorePathPtr,
        };

        var cb = new VcCallbacksNative
        {
            OnEvent = (nint)(delegate* unmanaged<nint, VcEventNative*, void>)&NativeCallbacks.OnEvent,
            OnLevel = (nint)(delegate* unmanaged<nint, uint, float, void>)&NativeCallbacks.OnLevel,
            User = GCHandle.ToIntPtr(_selfHandle),
        };

        nint native = NativeMethods.vc_client_create(in cfg, cb);
        _handle.SetHandle(native);
        if (_handle.IsInvalid)
        {
            FreeConfigStrings();
            _selfHandle.Free();
            throw new InvalidOperationException("vc_client_create failed.");
        }
    }

    /// <summary>
    /// Drains every event/level sample queued since the last call. Call this from a
    /// System.Windows.Forms.Timer.Tick on the UI thread (~30-50ms) — this is the boundary
    /// where the core's own event-delivery thread hands off to the UI thread; see
    /// docs/architecture.md §3 and this project's README for why a Timer + Channel was chosen
    /// over a message-only window + PostMessage.
    /// </summary>
    public void PumpEvents()
    {
        while (_events.Reader.TryRead(out var ev))
            EventReceived?.Invoke(ev);

        if (!_latestLevels.IsEmpty)
        {
            foreach (var (streamId, rms) in _latestLevels)
                LevelChanged?.Invoke(streamId, rms);
            _latestLevels.Clear();
        }
    }

    internal void EnqueueEvent(VoiceCatEvent ev)
    {
        // Temporary diagnostic (manual debugging session) — confirms the native callback
        // chain (UnmanagedCallersOnly -> GCHandle resolve -> here) actually fires, independent
        // of whether the UI-thread drain (PumpEvents) ever sees it.
        Console.WriteLine($"[VoiceCatClient] EnqueueEvent (native thread): {ev}");
        _events.Writer.TryWrite(ev);
    }
    internal void EnqueueLevel(uint streamId, float rms) => _latestLevels[streamId] = rms;

    // ── Connection & auth ────────────────────────────────────────────────────────────────
    public VcResult Connect(string host, ushort port) =>
        NativeMethods.vc_connect(_handle.DangerousGetHandle(), host, port);

    public VcResult Disconnect() =>
        NativeMethods.vc_disconnect(_handle.DangerousGetHandle());

    public VcResult AuthenticateGuest(string nickname) =>
        NativeMethods.vc_authenticate_guest(_handle.DangerousGetHandle(), nickname);

    public VcResult AuthenticateUser(string username, string password) =>
        NativeMethods.vc_authenticate_user(_handle.DangerousGetHandle(), username, password);

    // ── TOFU server-identity gate (M4) ──────────────────────────────────────────────────────
    public VcResult ConfirmServerIdentity(bool accept) =>
        NativeMethods.vc_confirm_server_identity(_handle.DangerousGetHandle(), accept ? 1 : 0);

    /// <summary>The Ed25519 identity fingerprint from ServerHello, hex-formatted — display
    /// only, NOT the value the TOFU gate pins on (see VcTofuStatus's doc comment). Empty
    /// string if not yet available.</summary>
    public string GetServerIdentityDisplay()
    {
        nint c = _handle.DangerousGetHandle();
        NativeMethods.vc_get_server_identity_display(c, 0, 0, out nuint len);
        if (len == 0) return string.Empty;

        nint buf = Marshal.AllocHGlobal((int)len + 1);
        try
        {
            NativeMethods.vc_get_server_identity_display(c, buf, len + (nuint)1, out _);
            return Marshal.PtrToStringUTF8(buf) ?? string.Empty;
        }
        finally
        {
            Marshal.FreeHGlobal(buf);
        }
    }

    // ── Channels ─────────────────────────────────────────────────────────────────────────
    /// <summary>Result arrives as a VcEventType.JoinResult event, not via this return value
    /// (which only reflects "request queued" — see voicecat.h's vc_join_channel doc comment).
    /// NOTE: no in-tree channel has a server-side password to check yet (M5+ feature) — this
    /// path is wired but not yet exercisable end-to-end.</summary>
    public VcResult JoinChannel(uint channelId, string? password = null) =>
        NativeMethods.vc_join_channel(_handle.DangerousGetHandle(), channelId, password);

    public VcResult LeaveChannel() =>
        NativeMethods.vc_leave_channel(_handle.DangerousGetHandle());

    public List<ChannelInfo> ListChannels()
    {
        NativeMethods.vc_list_channels(_handle.DangerousGetHandle(), out var native);
        return Marshaling.ToManaged(ref native);
    }

    public List<UserInfo> ListUsers()
    {
        NativeMethods.vc_list_users(_handle.DangerousGetHandle(), out var native);
        return Marshaling.ToManaged(ref native);
    }

    public List<StreamSummary> ListUserStreams(uint userId)
    {
        var r = NativeMethods.vc_list_user_streams(_handle.DangerousGetHandle(), userId, out var native);
        return r == VcResult.Ok ? Marshaling.ToManaged(ref native) : new List<StreamSummary>();
    }

    // ── Local media streams ─────────────────────────────────────────────────────────────────
    public (VcResult Result, uint StreamId) StartStream(VcStreamKind kind, string label)
    {
        nint labelPtr = Marshal.StringToCoTaskMemUTF8(label);
        try
        {
            var desc = new VcStreamDescNative { Kind = kind, DeviceId = 0, Label = labelPtr };
            var r = NativeMethods.vc_stream_start(_handle.DangerousGetHandle(), in desc, out uint streamId);
            return (r, streamId);
        }
        finally
        {
            Marshal.FreeCoTaskMem(labelPtr);
        }
    }

    /// <summary>
    /// Like <see cref="StartStream"/> but sets <c>external_feed = 1</c> so the core skips its
    /// own WASAPI loopback. The caller is responsible for feeding PCM via
    /// <see cref="StreamFeedPcm"/>. Used by the Windows per-app capture path.
    /// </summary>
    public (VcResult Result, uint StreamId) StartStreamExternalFeed(VcStreamKind kind, string label)
    {
        nint labelPtr = Marshal.StringToCoTaskMemUTF8(label);
        try
        {
            var desc = new VcStreamDescNative { Kind = kind, DeviceId = 0, Label = labelPtr, ExternalFeed = 1 };
            var r = NativeMethods.vc_stream_start(_handle.DangerousGetHandle(), in desc, out uint streamId);
            return (r, streamId);
        }
        finally
        {
            Marshal.FreeCoTaskMem(labelPtr);
        }
    }

    public VcResult StopStream(uint streamId) =>
        NativeMethods.vc_stream_stop(_handle.DangerousGetHandle(), streamId);

    public VcResult SetInputDevice(uint streamId, string? deviceId) =>
        NativeMethods.vc_set_input_device(_handle.DangerousGetHandle(), streamId, deviceId);

    /// <summary>Sets the mic capture channel count (1 = mono, 2 = stereo) for the given stream.
    /// Applied when the capture device next (re)starts — call before Join Voice, or pair with an
    /// audio restart to take effect live. Real stereo only reaches the wire on a stereo channel;
    /// the core folds a stereo mic to mono on a mono channel.</summary>
    public VcResult SetCaptureChannels(uint streamId, uint channels) =>
        NativeMethods.vc_set_capture_channels(_handle.DangerousGetHandle(), streamId, channels);

    /// <summary>Uninitializes and re-initializes the capture and playback devices on a running
    /// engine, applying pending changes (e.g. capture channel count) that only take effect on a
    /// device restart. No-op if audio isn't running.</summary>
    public VcResult AudioRestart() =>
        NativeMethods.vc_audio_restart(_handle.DangerousGetHandle());

    public VcResult SetInputMode(VcInputMode mode) =>
        NativeMethods.vc_set_input_mode(_handle.DangerousGetHandle(), mode);

    public VcResult SetVadThreshold(float threshold) =>
        NativeMethods.vc_set_vad_threshold(_handle.DangerousGetHandle(), threshold);

    public VcResult SetPushToTalk(bool active) =>
        NativeMethods.vc_set_push_to_talk(_handle.DangerousGetHandle(), active ? 1 : 0);

    public VcResult SetSelfMute(bool micMuted, bool deafened) =>
        NativeMethods.vc_set_self_mute(_handle.DangerousGetHandle(), micMuted ? 1 : 0, deafened ? 1 : 0);

    public VcResult SetOutputVolume(float gain) =>
        NativeMethods.vc_set_output_volume(_handle.DangerousGetHandle(), gain < 0f ? 0f : gain);

    /// <summary>Send-side microphone input gain, applied to captured MIC PCM before VAD/encode.
    /// 0.0 = silent, 1.0 = unity (default), &gt;1.0 amplifies (clamped to int16). LOCAL only.</summary>
    public VcResult SetInputGain(float gain) =>
        NativeMethods.vc_set_input_gain(_handle.DangerousGetHandle(), gain < 0f ? 0f : gain);

    /// <summary>Send-side microphone noise suppression (RNNoise). Denoises captured MIC PCM before
    /// the input gain and VAD/PTT gate, so everyone hears the cleaned signal. MIC stream only,
    /// mono only; always LOCAL — no protocol traffic. Independent of per-listener receive-side
    /// NR in <see cref="SetRemoteStream"/>.</summary>
    public VcResult SetInputNoiseReduction(bool enable) =>
        NativeMethods.vc_set_input_noise_reduction(_handle.DangerousGetHandle(), enable ? 1 : 0);

    public VcResult SetRemoteStream(uint userId, uint streamId, float gain, bool muted, bool noiseReduction) =>
        NativeMethods.vc_set_remote_stream(_handle.DangerousGetHandle(), userId, streamId, gain,
            muted ? 1 : 0, noiseReduction ? 1 : 0);

    public (VcResult Result, RemoteStreamState? State) GetRemoteStream(uint userId, uint streamId)
    {
        var r = NativeMethods.vc_get_remote_stream(_handle.DangerousGetHandle(), userId,
            streamId, out var native);
        return (r, r == VcResult.Ok ? Marshaling.ToManaged(in native) : null);
    }

    public (VcResult Result, AudioConfigInfo? Config) GetStreamAudioConfig(uint userId, uint streamId)
    {
        var r = NativeMethods.vc_get_stream_audio_config(_handle.DangerousGetHandle(), userId,
            streamId, out var native);
        return (r, r == VcResult.Ok ? Marshaling.ToManaged(in native) : null);
    }

    // ── External PCM feed / tap ─────────────────────────────────────────────────────────
    public unsafe VcResult StreamFeedPcm(uint streamId, ReadOnlySpan<short> pcm,
        int samplesPerChannel, uint channels)
    {
        fixed (short* p = pcm)
            return NativeMethods.vc_stream_feed_pcm(_handle.DangerousGetHandle(),
                streamId, p, (nuint)samplesPerChannel, channels);
    }

    // Pass Marshal.GetFunctionPointerForDelegate(cb) for a managed delegate, or
    // IntPtr.Zero to disable. Keep the delegate alive for the lifetime of the registration.
    public VcResult SetPcmSink(nint cb, IntPtr user) =>
        NativeMethods.vc_set_pcm_sink(_handle.DangerousGetHandle(), cb, user);

    // ── M5: Moderation & admin ───────────────────────────────────────────────────────────
    public VcResult KickUser(uint userId, string? reason = null) =>
        NativeMethods.vc_kick_user(_handle.DangerousGetHandle(), userId, reason);

    public VcResult BanUser(uint userId, string? reason = null, ulong expiresUnixMs = 0) =>
        NativeMethods.vc_ban_user(_handle.DangerousGetHandle(), userId, reason, expiresUnixMs);

    public VcResult SetPermission(uint userId, PermissionsInfo perms)
    {
        var native = new VcPermissionsNative
        {
            CanCreateTempChannel = perms.CanCreateTempChannel ? 1 : 0,
            CanKick = perms.CanKick ? 1 : 0,
            CanBan = perms.CanBan ? 1 : 0,
            CanMoveUsers = perms.CanMoveUsers ? 1 : 0,
            CanAdminAccounts = perms.CanAdminAccounts ? 1 : 0,
            IsAdmin = perms.IsAdmin ? 1 : 0,
        };
        return NativeMethods.vc_set_permission(_handle.DangerousGetHandle(), userId, in native);
    }

    public VcResult SetServerMute(uint userId, bool muted, bool deafened) =>
        NativeMethods.vc_set_server_mute(_handle.DangerousGetHandle(), userId,
            muted ? 1 : 0, deafened ? 1 : 0);

    public VcResult MoveUser(uint userId, uint channelId) =>
        NativeMethods.vc_move_user(_handle.DangerousGetHandle(), userId, channelId);

    public VcResult CreateChannel(ChannelEditInfo info)
    {
        var native = ToNativeChannelInfo(info);
        try
        {
            return NativeMethods.vc_create_channel(_handle.DangerousGetHandle(), in native);
        }
        finally
        {
            FreeChannelInfoStrings(native);
        }
    }

    public VcResult EditChannel(ChannelEditInfo info)
    {
        var native = ToNativeChannelInfo(info);
        try
        {
            return NativeMethods.vc_edit_channel(_handle.DangerousGetHandle(), in native);
        }
        finally
        {
            FreeChannelInfoStrings(native);
        }
    }

    public VcResult DeleteChannel(uint channelId) =>
        NativeMethods.vc_delete_channel(_handle.DangerousGetHandle(), channelId);

    public VcResult CreateAccount(string username, string password) =>
        NativeMethods.vc_create_account(_handle.DangerousGetHandle(), username, password);

    public VcResult ResetPassword(string username, string newPassword) =>
        NativeMethods.vc_reset_password(_handle.DangerousGetHandle(), username, newPassword);

    public VcResult DeleteAccount(string username) =>
        NativeMethods.vc_delete_account(_handle.DangerousGetHandle(), username);

    public VcResult RequestAccountList() =>
        NativeMethods.vc_list_accounts(_handle.DangerousGetHandle());

    public List<AccountInfo> ListAccounts()
    {
        NativeMethods.vc_get_account_list(_handle.DangerousGetHandle(), out var native);
        return Marshaling.ToManaged(ref native);
    }

    public PermissionsInfo GetPermissions()
    {
        NativeMethods.vc_get_permissions(_handle.DangerousGetHandle(), out var native);
        return Marshaling.ToManaged(in native);
    }

    private static VcChannelInfoNative ToNativeChannelInfo(ChannelEditInfo info)
    {
        return new VcChannelInfoNative
        {
            Id = info.Id,
            ParentId = info.ParentId,
            Name = Marshal.StringToCoTaskMemUTF8(info.Name),
            Topic = Marshal.StringToCoTaskMemUTF8(info.Topic),
            PasswordProtected = info.PasswordProtected ? 1 : 0,
            Password = string.IsNullOrEmpty(info.Password)
                ? 0
                : Marshal.StringToCoTaskMemUTF8(info.Password),
            MaxUsers = info.MaxUsers,
            SortOrder = info.SortOrder,
            Audio = new VcAudioConfigNative
            {
                Codec = info.Audio.Codec,
                Mode = info.Audio.Stereo ? 1u : 0u,
                SampleRate = info.Audio.SampleRate,
                BitrateBps = info.Audio.BitrateBps,
                FrameMs = info.Audio.FrameMs,
                Application = info.Audio.Application,
                Fec = info.Audio.Fec ? 1 : 0,
                ExpectedPacketLoss = info.Audio.ExpectedPacketLoss,
                Dtx = info.Audio.Dtx ? 1 : 0,
                Complexity = info.Audio.Complexity,
                Dred = info.Audio.Dred ? 1 : 0,
            }
        };
    }

    private static void FreeChannelInfoStrings(VcChannelInfoNative native)
    {
        if (native.Name != 0) Marshal.FreeCoTaskMem(native.Name);
        if (native.Topic != 0) Marshal.FreeCoTaskMem(native.Topic);
        if (native.Password != 0) Marshal.FreeCoTaskMem(native.Password);
    }

    // ── Text ─────────────────────────────────────────────────────────────────────────────
    public VcResult SendText(VcTextScope scope, uint targetId, string utf8) =>
        NativeMethods.vc_send_text(_handle.DangerousGetHandle(), scope, targetId, utf8);

    // ── Device enumeration (works pre-connect) ──────────────────────────────────────────────
    public List<DeviceInfo> ListDevices(VcDeviceKind kind)
    {
        NativeMethods.vc_list_devices(_handle.DangerousGetHandle(), kind, out var native);
        return Marshaling.ToManaged(ref native);
    }

    // ── Lifecycle ────────────────────────────────────────────────────────────────────────
    // NativeMethods.vc_version_string/vc_result_string return raw pointers to static, never-
    // freed string literals — see NativeMethods.cs's comment for why we don't let LibraryImport
    // auto-marshal these as `string` (it would try to free a static literal and corrupt the
    // heap). Marshal.PtrToStringUTF8 just reads; it never frees.
    public static string VersionString =>
        Marshal.PtrToStringUTF8(NativeMethods.vc_version_string()) ?? string.Empty;

    public static string ResultString(VcResult code) =>
        Marshal.PtrToStringUTF8(NativeMethods.vc_result_string(code)) ?? string.Empty;

    public void Dispose()
    {
        _handle.Dispose(); // runs vc_client_destroy (joins every internal thread) synchronously
        FreeConfigStrings();
        if (_selfHandle.IsAllocated) _selfHandle.Free();
    }

    private void FreeConfigStrings()
    {
        if (_clientNamePtr != 0) { Marshal.FreeCoTaskMem(_clientNamePtr); _clientNamePtr = 0; }
        if (_clientVersionPtr != 0) { Marshal.FreeCoTaskMem(_clientVersionPtr); _clientVersionPtr = 0; }
        if (_tofuStorePathPtr != 0) { Marshal.FreeCoTaskMem(_tofuStorePathPtr); _tofuStorePathPtr = 0; }
    }
}
