using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NcProgramManager;
using NcProgramManager.Cnc;
using NcProgramManager.Cnc.Models;

namespace NcProgramManager.Cnc.Fanuc
{
    public sealed class FanucMachine : IFanucMachine
    {
        private const int AlarmSlots = 17;
        private const int OperatorMsgSlots = 17;
        private const int TransferChunkSize = 1024;
        private const int UploadBufferSize = 10240;
        private const int Param_PiecesCount = 6711;
        private const int Param_CycleTimeMs = 6757;
        private const int Param_CycleTimeMin = 6758;
        private const string DefaultDownloadFolder = "//CNC_MEM/USER/PATH1/";
        private const string SeedProgramName = "OSTD";
        private const string SeedProgramBody = "%\n<OSTD>\n%";

        private static readonly Logger _log = LogManager.GetCurrentClassLogger();

        private readonly FanucConnectionConfig _config;
        private readonly INcProgramValidator _validator;
        private readonly List<CncError> _currentErrors = new List<CncError>();
        private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);

        private ushort _hndl;
        private FanucCapabilities _caps;
        private IFocasDialect _dialect;
        private ConnectionState _state = ConnectionState.Disconnected;

        public FanucMachine(FanucConnectionConfig config)
        {
            if (config == null) throw new ArgumentNullException("config");
            _config = config;
            _validator = new NcProgramValidator(CncManufacturer.Fanuc);
        }

        internal FanucMachine(FanucConnectionConfig config, INcProgramValidator validator)
        {
            if (config == null) throw new ArgumentNullException("config");
            _config = config;
            _validator = validator ?? new NcProgramValidator(CncManufacturer.Fanuc);
        }

        public string Name { get { return _config.Name; } }
        public ConnectionState State { get { return _state; } }
        public FanucCapabilities Capabilities { get { return _caps; } }

        public event EventHandler<ConnectionState> StateChanged;

        public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => ConnectCore(ct), ct);
        }

        public Task DisconnectAsync()
        {
            return RunGuardedAsync<bool>(() => { DisconnectCore(); return true; }, CancellationToken.None);
        }

        public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => ReadCore(ct), ct);
        }

        public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => ReadActiveProgramCore(ct), ct);
        }

        public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => ReadProgramCore(path, ct), ct);
        }

        public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => WriteProgramCore(path, program, ct), ct);
        }

        public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => SelectMainProgramCore(path), ct);
        }

        public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => DeleteProgramCore(path), ct);
        }

        public Task<CncResult<string>> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => UploadFocasFile(1, path, ct), ct);
        }

        public Task<CncResult<string>> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => UploadFocasFile(5, path, ct), ct);
        }

        public Task<CncResult<bool>> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => DownloadFocasFile(1, folderPath, content, ct), ct);
        }

        public Task<CncResult<bool>> WriteWorkZeroDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
        {
            return RunGuardedAsync(() => DownloadFocasFile(5, folderPath, content, ct), ct);
        }

        public void Dispose()
        {
            try { DisconnectAsync().GetAwaiter().GetResult(); } catch { /* cleanup — intentionally swallowed */ }
            try { _gate.Dispose(); } catch { /* cleanup — intentionally swallowed */ }
        }

        private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
        {
            await _gate.WaitAsync(ct).ConfigureAwait(false);
            try
            {
                return await Task.Run(() =>
                {
                    _currentErrors.Clear();
                    try
                    {
                        T value = body();
                        if (_currentErrors.Count > 0)
                            return CncResult<T>.Fail(_currentErrors.ToArray());
                        return CncResult<T>.Ok(value);
                    }
                    catch (OperationCanceledException)
                    {
                        return CncResult<T>.Cancelled();
                    }
                    catch (Exception ex)
                    {
                        _currentErrors.Add(new CncError(0, "FanucMachine", ex.Message));
                        return CncResult<T>.Fail(_currentErrors.ToArray());
                    }
                }, ct).ConfigureAwait(false);
            }
            finally
            {
                _gate.Release();
            }
        }

        private bool ConnectCore(CancellationToken ct)
        {
            if (_state == ConnectionState.Connected) return true;
            SetState(ConnectionState.Connecting);
            ct.ThrowIfCancellationRequested();

            short ret;
            switch (_config.Transport)
            {
                case FanucTransport.Ethernet:
                    short timeoutSec = (short)Math.Max(1, (int)_config.ConnectTimeout.TotalSeconds);
                    ret = Focas1.cnc_allclibhndl3(_config.IpAddress, _config.Port, timeoutSec, out _hndl);
                    break;
                case FanucTransport.Hssb:
                    ret = Focas1.cnc_allclibhndl2(_config.HssbNode, out _hndl);
                    break;
                case FanucTransport.Library:
                    ret = Focas1.cnc_allclibhndl(out _hndl);
                    break;
                default:
                    Record((short)Focas1.focas_ret.EW_ATTRIB, "Connect");
                    SetState(ConnectionState.Faulted);
                    return false;
            }

            if (ret != Focas1.EW_OK)
            {
                Record(ret, "cnc_allclibhndl");
                SetState(ConnectionState.Faulted);
                return false;
            }

            if (ct.IsCancellationRequested)
            {
                try { Focas1.cnc_freelibhndl(_hndl); } catch { /* cleanup — intentionally swallowed */ }
                SetState(ConnectionState.Disconnected);
                ct.ThrowIfCancellationRequested();
            }

            _caps = FanucDialectDetector.Detect(_hndl);
            _dialect = FocasDialectFactory.Create(_caps);
            if (_caps.Dialect == FanucDialect.Unknown)
                _currentErrors.Add(new CncError(0, "cnc_sysinfo", "Unknown CNC model, falling back to Legacy dialect"));

            SetState(ConnectionState.Connected);
            return true;
        }

        private void DisconnectCore()
        {
            if (_state == ConnectionState.Disconnected) return;
            short ret = Focas1.cnc_freelibhndl(_hndl);
            if (ret != Focas1.EW_OK)
                Record(ret, "cnc_freelibhndl");
            SetState(ConnectionState.Disconnected);
        }

        private MachineSnapshot ReadCore(CancellationToken ct)
        {
            EnsureConnected();
            ct.ThrowIfCancellationRequested();

            Focas1.cnc_setpath(_hndl, 1);

            var snap = new MachineSnapshot
            {
                CapturedAt = DateTime.Now,
                Alarms = new List<string>(),
                OperatorMessages = new List<string>(),
                Mode = MachineMode.Unknown,
                Run = RunState.Unknown,
                Motion = MotionState.Unknown
            };

            snap.CycleTime = ReadCycleTime();
            ct.ThrowIfCancellationRequested();
            snap.PieceCount = ReadPieceCount();
            ct.ThrowIfCancellationRequested();
            snap.Alarms = ReadAlarms();
            ct.ThrowIfCancellationRequested();
            snap.OperatorMessages = ReadOperatorMessages();
            ct.ThrowIfCancellationRequested();

            var stateInfo = ReadMachineState();
            snap.Mode = stateInfo.Mode;
            snap.Run = stateInfo.Run;
            snap.Motion = stateInfo.Motion;
            snap.Emergency = stateInfo.Emergency;
            snap.AlarmActive = stateInfo.Alarm;

            if (snap.Mode != MachineMode.Edit)
            {
                ct.ThrowIfCancellationRequested();
                var prog = ReadProgramInfo();
                snap.MainProgram = prog.MainProgram;
                snap.SubProgram = prog.SubProgram;
                snap.ProgramComment = prog.Comment;
            }

            return snap;
        }

        private string ReadActiveProgramCore(CancellationToken ct)
        {
            EnsureConnected();
            short ret;
            string identifier = _dialect.ReadMainProgramIdentifier(_hndl, out ret);
            if (ret != Focas1.EW_OK)
            {
                Record(ret, "ReadMainProgramIdentifier");
                return null;
            }
            return UploadProgramByIdentifier(identifier, ct);
        }

        private string ReadProgramCore(string path, CancellationToken ct)
        {
            EnsureConnected();
            if (string.IsNullOrEmpty(path))
            {
                _currentErrors.Add(new CncError(0, "ReadProgram", "Path is empty"));
                return null;
            }
            return UploadProgramByIdentifier(path, ct);
        }

        private bool WriteProgramCore(string folderPath, CncProgram program, CancellationToken ct)
        {
            EnsureConnected();
            if (program == null || string.IsNullOrEmpty(program.Content))
            {
                _currentErrors.Add(new CncError(0, "WriteProgram", "Program content is empty"));
                return false;
            }
            var vr = _validator.Validate(program.Name ?? folderPath, program.Content);
            if (!vr.Success)
            {
                string msg = string.Join("; ", vr.Errors
                    .Where(e => e.Severity == ValidationSeverity.Error)
                    .Select(e => e.Message));
                _currentErrors.Add(new CncError(0, "Validator", msg));
                return false;
            }
            if (!CanDownload()) return false;
            if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder;

            string fileName = ExtractFileNameFromContent(program);
            if (string.IsNullOrEmpty(fileName))
            {
                _currentErrors.Add(new CncError(0, "WriteProgram", "Cannot extract program name from content"));
                return false;
            }

            EnsureSeedProgram(folderPath, ct);
            ct.ThrowIfCancellationRequested();

            if (_dialect.SupportsPathBasedPrograms)
                Focas1.cnc_pdf_del(_hndl, folderPath + fileName);

            short ret = _dialect.DownloadStart(_hndl, folderPath);
            if (ret != Focas1.EW_OK)
            {
                Record(ret, "DownloadStart");
                _dialect.DownloadEnd(_hndl);
                return false;
            }

            try
            {
                if (!TransferChunked(program.Content, ct)) return false;
            }
            finally
            {
                _dialect.DownloadEnd(_hndl);
            }

            if (_dialect.SupportsPathBasedPrograms)
            {
                ret = Focas1.cnc_pdf_slctmain(_hndl, folderPath + fileName);
                if (ret != Focas1.EW_OK)
                {
                    Record(ret, "cnc_pdf_slctmain");
                    return false;
                }
            }
            return true;
        }

        private bool SelectMainProgramCore(string path)
        {
            EnsureConnected();
            if (!_dialect.SupportsPathBasedPrograms)
            {
                _currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT,
                    "SelectMainProgram", "Path-based selection not supported on this CNC"));
                return false;
            }
            short ret = Focas1.cnc_pdf_slctmain(_hndl, path);
            if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_slctmain"); return false; }
            return true;
        }

        private bool DeleteProgramCore(string path)
        {
            EnsureConnected();
            if (!_dialect.SupportsPathBasedPrograms)
            {
                _currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT,
                    "DeleteProgram", "Path-based delete not supported on this CNC"));
                return false;
            }
            short ret = Focas1.cnc_pdf_del(_hndl, path);
            if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_del"); return false; }
            return true;
        }

        private string UploadFocasFile(short fileType, string path, CancellationToken ct)
        {
            EnsureConnected();
            if (string.IsNullOrEmpty(path))
            {
                _currentErrors.Add(new CncError(0, "UploadFocasFile", "Path is empty"));
                return null;
            }
            long programNumber = TryParseProgramNumber(path);
            short ret = _dialect.UploadStart(_hndl, path, programNumber, fileType);
            if (ret != Focas1.EW_OK)
            {
                Record(ret, "UploadStart");
                _dialect.UploadEnd(_hndl);
                return null;
            }
            var output = new StringBuilder();
            char[] buffer = new char[UploadBufferSize + 1];
            try
            {
                while (true)
                {
                    ct.ThrowIfCancellationRequested();
                    int len = UploadBufferSize;
                    short r = _dialect.UploadData(_hndl, ref len, buffer);
                    if (r == (short)Focas1.focas_ret.EW_BUFFER)
                    {
                        if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
                        continue;
                    }
                    if (r == Focas1.EW_OK && len > 0)
                    {
                        output.Append(buffer, 0, len);
                        if (buffer[len - 1] == '%') break;
                    }
                    else
                    {
                        if (r != Focas1.EW_OK) Record(r, "UploadData");
                        break;
                    }
                }
            }
            finally { _dialect.UploadEnd(_hndl); }
            return output.ToString();
        }

        private bool DownloadFocasFile(short fileType, string folderPath, string content, CancellationToken ct)
        {
            EnsureConnected();
            if (string.IsNullOrEmpty(content))
            {
                _currentErrors.Add(new CncError(0, "DownloadFocasFile", "Content is empty"));
                return false;
            }
            if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder;
            short ret = _dialect.DownloadStart(_hndl, folderPath, fileType);
            if (ret != Focas1.EW_OK)
            {
                Record(ret, "DownloadStart");
                _dialect.DownloadEnd(_hndl);
                return false;
            }
            try { return TransferChunked(content, ct); }
            finally { _dialect.DownloadEnd(_hndl); }
        }

        private TimeSpan ReadCycleTime()
        {
            var ms = new Focas1.IODBPSD();
            var min = new Focas1.IODBPSD();
            short r1 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMs, 0, 8, ms);
            short r2 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMin, 0, 8, min);
            if (r1 != Focas1.EW_OK) Record(r1, "cnc_rdparam(cycle ms)");
            if (r2 != Focas1.EW_OK) Record(r2, "cnc_rdparam(cycle min)");
            return TimeSpan.FromMinutes(min.u.ldata) + TimeSpan.FromMilliseconds(ms.u.ldata);
        }

        private long ReadPieceCount()
        {
            try
            {
                if (_config.PieceCounterMacro == -1)
                {
                    var p = new Focas1.IODBPSD();
                    short ret = Focas1.cnc_rdparam(_hndl, Param_PiecesCount, 0, 8, p);
                    if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdparam(pieces)"); return 0; }
                    return p.u.ldata;
                }
                var macro = new Focas1.ODBM();
                short macRet = Focas1.cnc_rdmacro(_hndl, (short)_config.PieceCounterMacro, 10, macro);
                if (macRet != Focas1.EW_OK) { Record(macRet, "cnc_rdmacro(pieces)"); return 0; }
                long val = Math.Abs(macro.mcr_val);
                if (macro.dec_val > 0) val = val / (long)Math.Pow(10, macro.dec_val);
                return macro.mcr_val < 0 ? -val : val;
            }
            catch (Exception ex)
            {
                _currentErrors.Add(new CncError(0, "ReadPieceCount", ex.Message));
                return 0;
            }
        }

        private IReadOnlyList<string> ReadAlarms()
        {
            string[] buf = new string[AlarmSlots];
            short num = AlarmSlots;
            short ret = _dialect.ReadAlarmMessages(_hndl, ref num, buf);
            if (ret != Focas1.EW_OK) Record(ret, "ReadAlarmMessages");
            var list = new List<string>(num);
            for (int i = 0; i < num; i++)
                if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
            return list;
        }

        private IReadOnlyList<string> ReadOperatorMessages()
        {
            string[] buf = new string[OperatorMsgSlots];
            short num = 1;
            short ret = _dialect.ReadOperatorMessages(_hndl, ref num, buf);
            if (ret != Focas1.EW_OK) Record(ret, "ReadOperatorMessages");
            var list = new List<string>(num);
            for (int i = 0; i < num; i++)
                if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
            return list;
        }

        private struct StateInfo
        {
            public MachineMode Mode;
            public RunState Run;
            public MotionState Motion;
            public bool Emergency;
            public bool Alarm;
        }

        private StateInfo ReadMachineState()
        {
            var s = new StateInfo
            {
                Mode = MachineMode.Unknown,
                Run = RunState.Unknown,
                Motion = MotionState.Unknown
            };
            var odbst = new Focas1.ODBST();
            short ret = Focas1.cnc_statinfo(_hndl, odbst);
            if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo"); return s; }

            switch (odbst.aut)
            {
                case 0: s.Mode = MachineMode.Mdi; break;
                case 1: s.Mode = MachineMode.Auto; break;
                case 3: s.Mode = MachineMode.Edit; break;
                case 4: s.Mode = MachineMode.Handle; break;
                case 5: s.Mode = MachineMode.Jog; break;
            }
            switch (odbst.run)
            {
                case 1: s.Run = RunState.Stop; break;
                case 3: s.Run = RunState.ExecutingIso; break;
            }
            switch (odbst.motion)
            {
                case 0: s.Motion = MotionState.Idle; break;
                case 1: s.Motion = MotionState.Moving; break;
                case 2: s.Motion = MotionState.Hold; break;
            }
            s.Emergency = odbst.emergency == 1;
            s.Alarm = odbst.alarm == 1;
            return s;
        }

        private struct ProgramInfo
        {
            public string MainProgram;
            public string SubProgram;
            public string Comment;
        }

        private ProgramInfo ReadProgramInfo()
        {
            var info = new ProgramInfo { MainProgram = "", SubProgram = "", Comment = "" };

            var prog = new Focas1.ODBPRO();
            short ret = Focas1.cnc_rdprgnum(_hndl, prog);
            if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdprgnum"); return info; }
            info.SubProgram = "O" + prog.data.ToString();
            info.MainProgram = "O" + prog.mdata.ToString();

            if (_dialect.SupportsPathBasedPrograms)
            {
                byte[] mainPath = new byte[242];
                short pathRet = Focas1.cnc_pdf_rdmain(_hndl, mainPath);
                if (pathRet == Focas1.EW_OK)
                {
                    string str = ASCIIEncoding.ASCII.GetString(mainPath).TrimEnd('\0');
                    string[] parts = str.Split('/');
                    if (parts.Length > 0) info.MainProgram = parts[parts.Length - 1];
                }
            }

            byte[] commentBuf = new byte[100];
            int charsRead;
            short cret = _dialect.ReadActiveProgramComment(_hndl, commentBuf, out charsRead);
            if (cret != Focas1.EW_OK) { Record(cret, "ReadActiveProgramComment"); return info; }

            string raw = ASCIIEncoding.ASCII.GetString(commentBuf).TrimEnd('\0');
            int open = raw.IndexOf('(');
            int close = raw.IndexOf(')');
            if (open >= 0 && close > open)
                info.Comment = raw.Substring(open + 1, close - open - 1);
            else
                info.Comment = "";
            return info;
        }

        private string UploadProgramByIdentifier(string identifier, CancellationToken ct)
        {
            long programNumber = TryParseProgramNumber(identifier);
            short ret = _dialect.UploadStart(_hndl, identifier, programNumber);
            if (ret != Focas1.EW_OK)
            {
                Record(ret, "UploadStart");
                _dialect.UploadEnd(_hndl);
                return null;
            }

            var output = new StringBuilder();
            char[] buffer = new char[UploadBufferSize + 1];
            try
            {
                while (true)
                {
                    ct.ThrowIfCancellationRequested();
                    int len = UploadBufferSize;
                    short r = _dialect.UploadData(_hndl, ref len, buffer);
                    if (r == (short)Focas1.focas_ret.EW_BUFFER)
                    {
                        if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
                        continue;
                    }
                    if (r == Focas1.EW_OK && len > 0)
                    {
                        output.Append(buffer, 0, len);
                        if (len > 0 && buffer[len - 1] == '%') break;
                    }
                    else
                    {
                        if (r != Focas1.EW_OK) Record(r, "UploadData");
                        break;
                    }
                }
            }
            finally
            {
                _dialect.UploadEnd(_hndl);
            }
            return output.ToString();
        }

        private bool TransferChunked(string content, CancellationToken ct)
        {
            int len = content.Length;
            int startPos = 0;
            while (len > 0)
            {
                ct.ThrowIfCancellationRequested();
                int num = len >= TransferChunkSize ? TransferChunkSize : len;
                char[] chunk = new char[num];
                content.CopyTo(startPos, chunk, 0, num);

                short ret = _dialect.DownloadData(_hndl, ref num, chunk);
                if (ret == (short)Focas1.focas_ret.EW_BUFFER)
                {
                    if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
                    continue;
                }
                if (ret == Focas1.EW_OK)
                {
                    startPos += num;
                    len -= num;
                    continue;
                }
                Record(ret, "DownloadData");
                var dtl = new Focas1.ODBERR();
                Focas1.cnc_getdtailerr(_hndl, dtl);
                Debug.WriteLine("FOCAS detail err: " + dtl.err_no + " " + dtl.err_dtno);
                return false;
            }
            return true;
        }

        private bool CanDownload()
        {
            var odbst = new Focas1.ODBST();
            short ret = Focas1.cnc_statinfo(_hndl, odbst);
            if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo(canDownload)"); return false; }
            if (odbst.aut != 0 && odbst.aut != 3)
            {
                Record((short)Focas1.focas_ret.EW_MODE, "CanDownload");
                return false;
            }
            return true;
        }

        private void EnsureSeedProgram(string folderPath, CancellationToken ct)
        {
            if (!_dialect.SupportsPathBasedPrograms) return;
            short selRet = Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName);
            if (selRet == Focas1.EW_OK) return;

            Focas1.cnc_pdf_del(_hndl, folderPath + SeedProgramName);
            short sStart = _dialect.DownloadStart(_hndl, folderPath);
            if (sStart != Focas1.EW_OK) { _dialect.DownloadEnd(_hndl); return; }
            TransferChunked(SeedProgramBody, ct);
            _dialect.DownloadEnd(_hndl);
            Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName);
        }

        private static string ExtractFileNameFromContent(CncProgram program)
        {
            if (!string.IsNullOrEmpty(program.Name)) return program.Name;
            string content = program.Content ?? "";
            int start = content.IndexOf('<');
            int end = content.IndexOf('>');
            if (start >= 0 && end > start) return content.Substring(start + 1, end - start - 1);
            return null;
        }

        private static long TryParseProgramNumber(string identifier)
        {
            if (string.IsNullOrEmpty(identifier)) return 0;
            string s = identifier;
            int slash = s.LastIndexOf('/');
            if (slash >= 0 && slash < s.Length - 1) s = s.Substring(slash + 1);
            if (s.Length > 0 && (s[0] == 'O' || s[0] == 'o')) s = s.Substring(1);
            long n;
            return long.TryParse(s, out n) ? n : 0;
        }

        private void EnsureConnected()
        {
            if (_state != ConnectionState.Connected || _dialect == null)
                throw new InvalidOperationException("FanucMachine is not connected.");
        }

        private void Record(short code, string source)
        {
            var err = FanucErrorTranslator.Translate(code, source);
            _currentErrors.Add(err);
            if (FanucErrorTranslator.IsDisconnectError(code))
            {
                SetState(ConnectionState.Faulted);
                try { Focas1.cnc_freelibhndl(_hndl); } catch { /* cleanup — intentionally swallowed */ }
                SetState(ConnectionState.Disconnected);
            }
        }

        private void SetState(ConnectionState next)
        {
            if (_state == next) return;
            _state = next;
            var h = StateChanged;
            if (h != null)
            {
                try { h(this, next); } catch (Exception ex) { _log.Warn(ex, "StateChanged handler threw"); }
            }
        }
    }
}
