using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace NcProgramManager.Tests.Integration.Stubs
{
    /// <summary>
    /// Minimal in-process FTP server using passive mode.
    /// Supports USER, PASS, SYST, PWD, CWD, TYPE, PASV, LIST, NLST, RETR, STOR, DELE, QUIT.
    /// Also accepts OPTS, FEAT, NOOP, CLNT, MODE, STRU as no-ops.
    /// All replies use CRLF (per RFC 959), independent of host platform.
    /// </summary>
    public sealed class FtpServerStub : IDisposable
    {
        private const string CRLF = "\r\n";

        private readonly TcpListener _listener;
        private readonly Thread _acceptThread;
        private volatile bool _running;

        public int Port { get; }

        /// <summary>In-memory file store: full path → content.</summary>
        public Dictionary<string, string> Files { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        public FtpServerStub()
        {
            _listener = new TcpListener(IPAddress.Loopback, 0);
            _listener.Start();
            Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
            _running = true;
            _acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "FtpStub-Accept" };
            _acceptThread.Start();
        }

        public void Dispose()
        {
            _running = false;
            try { _listener.Stop(); } catch { }
        }

        private void AcceptLoop()
        {
            while (_running)
            {
                TcpClient client;
                try { client = _listener.AcceptTcpClient(); }
                catch { break; }
                var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "FtpStub-Client" };
                t.Start();
            }
        }

        private static void SendLine(Stream stream, string line)
        {
            byte[] bytes = Encoding.ASCII.GetBytes(line + CRLF);
            stream.Write(bytes, 0, bytes.Length);
            stream.Flush();
        }

        private static string ReadLine(Stream stream)
        {
            var sb = new StringBuilder();
            int prev = -1;
            while (true)
            {
                int b = stream.ReadByte();
                if (b < 0) return sb.Length == 0 ? null : sb.ToString();
                if (b == '\n')
                {
                    if (prev == '\r')
                        sb.Length = sb.Length - 1; // strip CR
                    return sb.ToString();
                }
                sb.Append((char)b);
                prev = b;
            }
        }

        private void HandleClient(TcpClient client)
        {
            using (client)
            using (var stream = client.GetStream())
            {
                string cwd = "/";
                TcpListener dataListener = null;

                SendLine(stream, "220 FtpServerStub ready");

                string line;
                while ((line = ReadLine(stream)) != null)
                {
                    line = line.Trim();
                    if (string.IsNullOrEmpty(line)) continue;

                    string cmd, arg;
                    int sp = line.IndexOf(' ');
                    if (sp >= 0)
                    {
                        cmd = line.Substring(0, sp).ToUpperInvariant();
                        arg = line.Substring(sp + 1).Trim();
                    }
                    else
                    {
                        cmd = line.ToUpperInvariant();
                        arg = "";
                    }

                    switch (cmd)
                    {
                        case "USER":
                            SendLine(stream, "331 Password required");
                            break;

                        case "PASS":
                            SendLine(stream, "230 Logged in");
                            break;

                        case "SYST":
                            SendLine(stream, "215 UNIX Type: L8");
                            break;

                        case "FEAT":
                            SendLine(stream, "211-Features:");
                            SendLine(stream, " UTF8");
                            SendLine(stream, "211 End");
                            break;

                        case "OPTS":
                        case "CLNT":
                        case "NOOP":
                        case "MODE":
                        case "STRU":
                        case "ALLO":
                            SendLine(stream, "200 OK");
                            break;

                        case "PWD":
                            SendLine(stream, "257 \"" + cwd + "\"");
                            break;

                        case "CWD":
                            cwd = arg.StartsWith("/") ? arg : cwd.TrimEnd('/') + "/" + arg;
                            if (!cwd.EndsWith("/")) cwd += "/";
                            SendLine(stream, "250 CWD successful");
                            break;

                        case "TYPE":
                            SendLine(stream, "200 Type set");
                            break;

                        case "PASV":
                            dataListener = new TcpListener(IPAddress.Loopback, 0);
                            dataListener.Start();
                            int dataPort = ((IPEndPoint)dataListener.LocalEndpoint).Port;
                            int p1 = dataPort >> 8;
                            int p2 = dataPort & 0xFF;
                            SendLine(stream, "227 Entering Passive Mode (127,0,0,1," + p1 + "," + p2 + ")");
                            break;

                        case "LIST":
                        case "NLST":
                            if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
                            SendLine(stream, "150 Opening data connection");
                            try
                            {
                                using (var dc = dataListener.AcceptTcpClient())
                                using (var ds = dc.GetStream())
                                {
                                    // Empty listing — enough for Ping()
                                }
                            }
                            catch { }
                            try { dataListener.Stop(); } catch { }
                            dataListener = null;
                            SendLine(stream, "226 Transfer complete");
                            break;

                        case "RETR":
                            if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
                            string retrPath = NormalizePath(cwd, arg);
                            string retrContent;
                            if (!Files.TryGetValue(retrPath, out retrContent))
                            {
                                try { dataListener.Stop(); } catch { }
                                dataListener = null;
                                SendLine(stream, "550 File not found");
                                break;
                            }
                            SendLine(stream, "150 Opening data connection");
                            try
                            {
                                using (var dc = dataListener.AcceptTcpClient())
                                using (var ds = dc.GetStream())
                                {
                                    byte[] bytes = Encoding.ASCII.GetBytes(retrContent);
                                    ds.Write(bytes, 0, bytes.Length);
                                }
                            }
                            catch { }
                            try { dataListener.Stop(); } catch { }
                            dataListener = null;
                            SendLine(stream, "226 Transfer complete");
                            break;

                        case "STOR":
                            if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
                            string storPath = NormalizePath(cwd, arg);
                            SendLine(stream, "150 Opening data connection");
                            var sb = new StringBuilder();
                            try
                            {
                                using (var dc = dataListener.AcceptTcpClient())
                                using (var ds = dc.GetStream())
                                using (var dr = new StreamReader(ds, Encoding.ASCII))
                                {
                                    sb.Append(dr.ReadToEnd());
                                }
                            }
                            catch { }
                            Files[storPath] = sb.ToString();
                            try { dataListener.Stop(); } catch { }
                            dataListener = null;
                            SendLine(stream, "226 Transfer complete");
                            break;

                        case "DELE":
                            string delePath = NormalizePath(cwd, arg);
                            Files.Remove(delePath);
                            SendLine(stream, "250 DELE successful");
                            break;

                        case "QUIT":
                            SendLine(stream, "221 Goodbye");
                            return;

                        default:
                            SendLine(stream, "500 Unknown command");
                            break;
                    }
                }
            }
        }

        private static string NormalizePath(string cwd, string arg)
        {
            if (arg.StartsWith("/")) return arg;
            return cwd.TrimEnd('/') + "/" + arg;
        }
    }
}
