using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging;
using TeamsISO.App.ViewModels;
using TeamsISO.Engine.Controller;

namespace TeamsISO.App.Services;

/// <summary>
/// OSC over UDP. Companion, TouchOSC, Bome, and most lighting consoles speak
/// OSC natively, so wrapping the same command surface in OSC opens the
/// product to the broader live-show ecosystem without a Companion bridge.
///
/// Protocol — minimal OSC 1.0:
///   - Address pattern (null-terminated string, padded to 4-byte boundary)
///   - Type tag (",iiisf" etc., null-terminated, padded to 4)
///   - Args in order
///
/// We don't implement bundles, time tags, blob args, or pattern matching
/// — none are needed for the verbs we support. If a sender uses bundles
/// we ignore them; if a sender uses a wildcard address ("/teamsiso/*") we
/// ignore it. Operators get a clear log line in either case.
///
/// Routes:
///   /teamsiso/iso "DisplayName" {0|1}        — toggle/set ISO by display name
///   /teamsiso/iso/by-id "guid" {0|1}         — toggle/set by Id
///   /teamsiso/preset "Name"                  — apply preset
///   /teamsiso/teams/mute                     — UIA toggle mute
///   /teamsiso/teams/camera                   — UIA toggle camera
///   /teamsiso/teams/leave                    — UIA leave
///   /teamsiso/teams/share                    — UIA share tray
///   /teamsiso/teams/raise-hand               — UIA raise hand
///   /teamsiso/refresh-discovery              — rebuild NDI finder
///   /teamsiso/stop-all                       — disable every ISO
///   /teamsiso/recording {0|1}                — recording on/off (default dir)
/// </summary>
public sealed class OscBridge : IAsyncDisposable
{
    public const int DefaultPort = 9000;

    private readonly IIsoController _controller;
    private readonly Func<MainViewModel?> _viewModel;
    private readonly ILogger<OscBridge>? _logger;
    private UdpClient? _udp;
    private CancellationTokenSource? _cts;
    private Task? _receiveTask;

    public bool IsRunning { get; private set; }
    public int Port { get; private set; } = DefaultPort;
    /// <summary>True when the listener is bound to all interfaces rather than just loopback.</summary>
    public bool BoundToLan { get; private set; }

    public OscBridge(
        IIsoController controller,
        Func<MainViewModel?> viewModel,
        ILogger<OscBridge>? logger = null)
    {
        _controller = controller;
        _viewModel = viewModel;
        _logger = logger;
    }

    /// <summary>
    /// Start the OSC listener on the given UDP port. <paramref name="bindToLan"/>
    /// flag selects between loopback (default — only this machine) and any-
    /// interface binding (LAN-reachable, for thin-client controllers).
    /// Unlike the REST surface, UDP doesn't need a URL ACL — binding 0.0.0.0
    /// is just an unprivileged port reservation.
    /// </summary>
    public void Start(int port, bool bindToLan = false)
    {
        if (IsRunning && Port == port && BoundToLan == bindToLan) return;
        Stop();

        Port = port;
        BoundToLan = bindToLan;
        var bindAddr = bindToLan ? IPAddress.Any : IPAddress.Loopback;
        try
        {
            _udp = new UdpClient(new IPEndPoint(bindAddr, port));
        }
        catch (SocketException ex)
        {
            _logger?.LogWarning(ex, "Could not bind OSC bridge to udp://{Addr}:{Port}.", bindAddr, port);
            _udp = null;
            return;
        }
        _cts = new CancellationTokenSource();
        _receiveTask = Task.Run(() => ReceiveLoopAsync(_cts.Token));
        IsRunning = true;
        _logger?.LogInformation("OSC bridge listening on udp://{Addr}:{Port}/", bindAddr, port);
    }

    public void Stop()
    {
        if (!IsRunning) return;
        try { _cts?.Cancel(); } catch { /* ignore */ }
        try { _udp?.Close(); } catch { /* ignore */ }
        try { _receiveTask?.Wait(TimeSpan.FromSeconds(2)); } catch { /* ignore */ }
        _udp?.Dispose();
        _udp = null;
        _cts?.Dispose();
        _cts = null;
        _receiveTask = null;
        IsRunning = false;
    }

    public async ValueTask DisposeAsync()
    {
        Stop();
        await Task.CompletedTask;
    }

    private async Task ReceiveLoopAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested && _udp is not null)
        {
            UdpReceiveResult result;
            try { result = await _udp.ReceiveAsync(ct); }
            catch (OperationCanceledException) { break; }
            catch (ObjectDisposedException) { break; }
            catch (SocketException ex)
            {
                _logger?.LogWarning(ex, "OSC receive failed; continuing.");
                continue;
            }

            try
            {
                var msg = OscMessage.TryParse(result.Buffer);
                if (msg is null) continue;
                await DispatchAsync(msg);
            }
            catch (Exception ex)
            {
                _logger?.LogWarning(ex, "OSC dispatch failed for packet from {Endpoint}.", result.RemoteEndPoint);
            }
        }
    }

    private async Task DispatchAsync(OscMessage msg)
    {
        var addr = msg.Address;
        switch (addr)
        {
            case "/teamsiso/teams/mute":      InvokeTeams(TeamsControlBridge.ToggleMute); return;
            case "/teamsiso/teams/camera":    InvokeTeams(TeamsControlBridge.ToggleCamera); return;
            case "/teamsiso/teams/leave":     InvokeTeams(TeamsControlBridge.LeaveCall); return;
            case "/teamsiso/teams/share":     InvokeTeams(TeamsControlBridge.OpenShareTray); return;
            case "/teamsiso/teams/raise-hand":InvokeTeams(TeamsControlBridge.ToggleRaiseHand); return;
            case "/teamsiso/refresh-discovery":_controller.RefreshDiscovery(); return;
            case "/teamsiso/stop-all":        await StopAllAsync(); return;
            case "/teamsiso/recording":       SetRecording(msg); return;
            case "/teamsiso/recording/marker": DropMarker(msg); return;
            case "/teamsiso/recording/roll":  await RollRecordingAsync(); return;
            case "/teamsiso/notes":           AppendNote(msg); return;
            case "/teamsiso/iso":             await ToggleByNameAsync(msg); return;
            case "/teamsiso/iso/by-id":       await ToggleByIdAsync(msg); return;
            case "/teamsiso/preset":          await ApplyPresetAsync(msg); return;
            default:
                _logger?.LogDebug("Ignoring unknown OSC address {Addr}", addr);
                return;
        }
    }

    // ─── handler helpers ────────────────────────────────────────────────

    private static void InvokeTeams(Func<TeamsControlBridge.InvokeResult> action) => _ = action();

    private async Task StopAllAsync()
    {
        var vm = _viewModel();
        if (vm is null) return;
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (dispatcher is null) return;
        var enabled = await dispatcher.InvokeAsync(() =>
            vm.Participants.Where(p => p.IsEnabled).ToArray());
        foreach (var p in enabled)
        {
            try { await _controller.DisableIsoAsync(p.Id, CancellationToken.None); }
            catch { /* defensive */ }
            await dispatcher.InvokeAsync(() => p.IsEnabled = false);
        }
    }

    private void SetRecording(OscMessage msg)
    {
        var enabled = msg.GetBoolArg(0) ?? false;
        // OSC doesn't carry a directory string in this minimal protocol; let the
        // recording directory remain whatever the UI / REST surface set last.
        _controller.SetRecording(enabled, _controller.RecordingDirectory);
    }

    private void DropMarker(OscMessage msg)
    {
        var label = msg.GetStringArg(0) ?? "Marker @ " + DateTimeOffset.Now.ToString("HH:mm:ss");
        _controller.AddRecordingMarker(label);
    }

    private static void AppendNote(OscMessage msg)
    {
        var text = msg.GetStringArg(0);
        if (!string.IsNullOrWhiteSpace(text)) NotesService.Append(text);
    }

    /// <summary>Roll every active recording into a new chunk. Same path as REST /recording/roll.</summary>
    private async Task RollRecordingAsync()
    {
        var vm = _viewModel();
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (vm is null || dispatcher is null) return;

        var enabled = await dispatcher.InvokeAsync(() =>
            vm.Participants.Where(p => p.IsEnabled).ToArray());
        foreach (var p in enabled)
        {
            try
            {
                await _controller.DisableIsoAsync(p.Id, CancellationToken.None);
                await Task.Delay(150);
                var nameToUse = string.IsNullOrWhiteSpace(p.CustomName)
                    ? OutputNameTemplate.Render(OutputNameTemplate.Get(), p.Id, p.DisplayName)
                    : p.CustomName;
                bool? recordOverride = p.RecordToDisk ? null : false;
                await _controller.EnableIsoAsync(p.Id, nameToUse, recordOverride, CancellationToken.None);
            }
            catch { /* per-pipeline best-effort */ }
        }
    }

    private async Task ToggleByNameAsync(OscMessage msg)
    {
        var name = msg.GetStringArg(0);
        if (string.IsNullOrEmpty(name)) return;
        var enabled = msg.GetBoolArg(1);

        var vm = _viewModel();
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (vm is null || dispatcher is null) return;
        var p = await dispatcher.InvokeAsync(() =>
            vm.Participants.FirstOrDefault(x =>
                string.Equals(x.DisplayName, name, StringComparison.OrdinalIgnoreCase)));
        if (p is null) return;
        await ApplyToggleAsync(p, enabled, dispatcher);
    }

    private async Task ToggleByIdAsync(OscMessage msg)
    {
        var idStr = msg.GetStringArg(0);
        if (!Guid.TryParse(idStr, out var id)) return;
        var enabled = msg.GetBoolArg(1);

        var vm = _viewModel();
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (vm is null || dispatcher is null) return;
        var p = await dispatcher.InvokeAsync(() =>
            vm.Participants.FirstOrDefault(x => x.Id == id));
        if (p is null) return;
        await ApplyToggleAsync(p, enabled, dispatcher);
    }

    private async Task ApplyToggleAsync(ParticipantViewModel p, bool? enabled, System.Windows.Threading.Dispatcher dispatcher)
    {
        var target = enabled ?? !p.IsEnabled;
        if (target == p.IsEnabled) return;
        try
        {
            if (target)
            {
                await _controller.EnableIsoAsync(p.Id,
                    string.IsNullOrWhiteSpace(p.CustomName) ? null : p.CustomName,
                    CancellationToken.None);
                await dispatcher.InvokeAsync(() => p.IsEnabled = true);
            }
            else
            {
                await _controller.DisableIsoAsync(p.Id, CancellationToken.None);
                await dispatcher.InvokeAsync(() => p.IsEnabled = false);
            }
        }
        catch
        {
            /* defensive: OSC senders are typically fire-and-forget */
        }
    }

    private async Task ApplyPresetAsync(OscMessage msg)
    {
        var name = msg.GetStringArg(0);
        if (string.IsNullOrEmpty(name)) return;
        var preset = OperatorPresetStore.Find(name);
        if (preset is null) return;

        var vm = _viewModel();
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (vm is null || dispatcher is null) return;
        var snapshot = await dispatcher.InvokeAsync(() => vm.Participants.ToList());
        await PresetApplier.ApplyAsync(preset, snapshot, _controller, dispatcher);
    }
}

// ─── OSC message parser ─────────────────────────────────────────────────

/// <summary>
/// Minimal OSC 1.0 message parser. Supports the subset we care about:
/// integer (i), float (f), string (s) args. Bundles / time tags / blobs are
/// not implemented — incoming packets that look like bundles return null
/// and the caller logs + skips them.
/// </summary>
internal sealed class OscMessage
{
    public string Address { get; init; } = "";
    public string TypeTag { get; init; } = "";
    public IReadOnlyList<object> Args { get; init; } = Array.Empty<object>();

    /// <summary>Parse a single OSC packet. Returns null if malformed or a bundle.</summary>
    public static OscMessage? TryParse(byte[] bytes)
    {
        if (bytes.Length < 8) return null;
        // Bundle marker — we don't support bundles. Skip.
        if (bytes[0] == '#') return null;

        var idx = 0;
        var address = ReadOscString(bytes, ref idx);
        if (address is null || !address.StartsWith('/')) return null;

        if (idx >= bytes.Length) return new OscMessage { Address = address };
        var typeTag = ReadOscString(bytes, ref idx);
        if (typeTag is null || !typeTag.StartsWith(',')) return null;

        var args = new List<object>();
        for (var i = 1; i < typeTag.Length; i++)
        {
            switch (typeTag[i])
            {
                case 'i':
                    if (idx + 4 > bytes.Length) return null;
                    args.Add(ReadInt32BE(bytes, idx));
                    idx += 4;
                    break;
                case 'f':
                    if (idx + 4 > bytes.Length) return null;
                    args.Add(ReadFloat32BE(bytes, idx));
                    idx += 4;
                    break;
                case 's':
                    var s = ReadOscString(bytes, ref idx);
                    if (s is null) return null;
                    args.Add(s);
                    break;
                case 'T': args.Add(true); break;
                case 'F': args.Add(false); break;
                default:
                    // Unknown type — bail rather than mis-aligning subsequent args.
                    return null;
            }
        }
        return new OscMessage { Address = address, TypeTag = typeTag, Args = args };
    }

    public string? GetStringArg(int idx) =>
        idx < Args.Count && Args[idx] is string s ? s : null;

    public bool? GetBoolArg(int idx)
    {
        if (idx >= Args.Count) return null;
        return Args[idx] switch
        {
            bool b => b,
            int i => i != 0,
            float f => f != 0f,
            string s => s.Equals("true", StringComparison.OrdinalIgnoreCase) || s == "1",
            _ => null,
        };
    }

    private static string? ReadOscString(byte[] bytes, ref int idx)
    {
        var start = idx;
        while (idx < bytes.Length && bytes[idx] != 0) idx++;
        if (idx >= bytes.Length) return null;
        var s = Encoding.ASCII.GetString(bytes, start, idx - start);
        // Advance past the trailing null and align to 4-byte boundary.
        idx++;
        var pad = (4 - (idx - start) % 4) % 4;
        idx += pad;
        return s;
    }

    private static int ReadInt32BE(byte[] bytes, int offset) =>
        (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];

    private static float ReadFloat32BE(byte[] bytes, int offset)
    {
        Span<byte> tmp = stackalloc byte[4];
        tmp[0] = bytes[offset + 3];
        tmp[1] = bytes[offset + 2];
        tmp[2] = bytes[offset + 1];
        tmp[3] = bytes[offset];
        return BitConverter.ToSingle(tmp);
    }
}
