using System.IO.Ports;

namespace IOModuleTestBlazor.Services;

public sealed class SerialPortService : ISerialPortService, IDisposable
{
    private readonly Lock _lock = new();
    private SerialPort? _port;
    private readonly List<string> _lines = new(200);
    private string _receiveBuffer = string.Empty;

    private const int MaxLines = 200;

    public bool IsOpen => _port?.IsOpen == true;
    public string PortName => _port?.PortName ?? string.Empty;

    public event Action? DataReceived;

    public IReadOnlyList<string> GetPortNames()
        => SerialPort.GetPortNames();

    public void Open(string portName, int baudRate)
    {
        SerialPort newPort;
        lock (_lock)
        {
            ClosePortUnsafe();
            newPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
            {
                NewLine      = "\r\n",
                ReadTimeout  = SerialPort.InfiniteTimeout,
                WriteTimeout = 500,
                Encoding     = System.Text.Encoding.ASCII,
            };
        }

        // Open outside the lock — USB VCP init can block ~1 s; don't hold _lock during that.
        try
        {
            newPort.Open();
        }
        catch
        {
            newPort.Dispose();
            throw;
        }

        lock (_lock)
        {
            _port = newPort;
            _port.DataReceived += OnDataReceived;
        }
    }

    public void Close()
    {
        lock (_lock)
            ClosePortUnsafe();
    }

    // Must be called with _lock held. System.Threading.Lock is non-reentrant,
    // so Close() cannot be called from Open() while the lock is already held.
    private void ClosePortUnsafe()
    {
        if (_port is null) return;
        _port.DataReceived -= OnDataReceived;
        try { _port.Close(); } catch { /* ignore */ }
        _port.Dispose();
        _port = null;
    }

    public void WriteLine(string command)
    {
        SerialPort? port;
        lock (_lock) { port = _port; }

        if (port?.IsOpen != true) return;

        try
        {
            port.WriteLine(command);
        }
        catch (Exception ex) when (ex is IOException or TimeoutException or InvalidOperationException or UnauthorizedAccessException)
        {
            // Device stopped responding or VCP disconnected — close so IsOpen reflects reality.
            Close();
            DataReceived?.Invoke();
            return;
        }

        AppendLine($"> {command}");
    }

    public IReadOnlyList<string> GetLines()
    {
        lock (_lock)
            return _lines.ToList();
    }

    public void ClearLines()
    {
        lock (_lock)
            _lines.Clear();
    }

    private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        SerialPort? port;
        lock (_lock) { port = _port; }
        if (port is null) return;

        try
        {
            string incoming = port.ReadExisting();
            _receiveBuffer += incoming;

            // Split on newlines, keep partial last line in buffer
            var parts = _receiveBuffer.Split('\n');
            for (int i = 0; i < parts.Length - 1; i++)
            {
                var line = parts[i].TrimEnd('\r');
                if (line.Length > 0)
                    AppendLine($"< {line}");
            }
            _receiveBuffer = parts[^1];
        }
        catch { /* port closed mid-read */ }

        DataReceived?.Invoke();
    }

    private void AppendLine(string line)
    {
        lock (_lock)
        {
            if (_lines.Count >= MaxLines)
                _lines.RemoveAt(0);
            _lines.Add(line);
        }
    }

    public void Dispose() => Close();
}
