using System.Net;
using System.Net.Sockets;
using Sunfish.Auth;
using Sunfish.Config;
using Sunfish.Ftp;
using Sunfish.Http;
using Sunfish.Log;
using Sunfish.Net;
using Sunfish.Web;

namespace Sunfish.Host;

/// <summary>
/// Punto de entrada para las aplicaciones (GUI, TUI, consola): arranca y para los servicios
/// (HTTP y FTP), aplica y guarda la configuracion, vigila el fichero por si otro programa lo
/// cambia y expone el estado. Las aplicaciones no contienen logica de servidor.
/// </summary>
public sealed class SunfishHost : IAsyncDisposable
{
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly Authenticator _auth = new();
    private volatile ServerState _state;
    private HttpServer? _http;
    private FtpServer? _ftp;
    private MdnsAnnouncer? _mdns;
    private bool _running;
    private FileSystemWatcher? _watcher;
    private string? _lastWritten;
    private CancellationTokenSource? _reloadDelay;

    public SunfishHost(SunfishConfig config, ServerLog? log = null)
    {
        _state = new ServerState(config);
        Log = log ?? new ServerLog();
    }

    public ServerLog Log { get; }

    public SunfishConfig Config => _state.Config;

    /// <summary>
    /// Ajustes que mandan sobre el fichero (p. ej. -p en la linea de comandos); se vuelven a
    /// aplicar a cada configuracion recargada del disco.
    /// </summary>
    public Action<SunfishConfig>? Overrides { get; set; }

    /// <summary>Cambio de estado (arranque, parada, configuracion nueva). Llega en cualquier hilo.</summary>
    public event Action? Changed;

    /// <summary>El servidor esta en marcha (con los servicios que la configuracion active).</summary>
    public bool Running => _running;

    public bool HttpRunning => _http is not null;
    public bool FtpRunning => _ftp is not null;

    /// <summary>Donde escucha HTTP (puerto real si se pidio el 0).</summary>
    public IReadOnlyList<IPEndPoint> HttpEndpoints => _http?.Endpoints ?? [];
    public IReadOnlyList<IPEndPoint> FtpEndpoints => _ftp?.Endpoints ?? [];

    /// <summary>Puerto HTTP en uso (o el configurado si esta parado).</summary>
    public int HttpPort => HttpEndpoints.Count > 0 ? HttpEndpoints[0].Port : Config.Server.HttpPort;
    public int FtpPort => FtpEndpoints.Count > 0 ? FtpEndpoints[0].Port : Config.Server.FtpPort;

    /// <summary>
    /// Arranca los servicios activados en la configuracion. Lanza <see cref="SocketException"/>
    /// si un puerto esta ocupado (los que ya arrancaron siguen en marcha).
    /// </summary>
    public void Start()
    {
        _gate.Wait();
        try
        {
            _running = true;
            try
            {
                StartServicesLocked();
            }
            finally
            {
                UpdateAnnouncerLocked().GetAwaiter().GetResult();
            }
        }
        finally
        {
            _gate.Release();
            Changed?.Invoke();
        }
    }

    public async Task StopAsync()
    {
        await _gate.WaitAsync();
        try
        {
            _running = false;
            await StopHttpLocked();
            await StopFtpLocked();
            await UpdateAnnouncerLocked();
        }
        finally
        {
            _gate.Release();
        }
        Changed?.Invoke();
    }

    /// <summary>Anuncio DNS-SD acorde al estado: en marcha con Announce, o parado.</summary>
    private async Task UpdateAnnouncerLocked()
    {
        bool wanted = _running && Config.Server.Announce && (_http is not null || _ftp is not null);
        if (!wanted)
        {
            if (_mdns is { } old)
            {
                _mdns = null;
                await old.DisposeAsync();
            }
            return;
        }
        var services = new AnnouncedServices(Config.Server.Name,
            _http is not null ? HttpPort : 0,
            Config.Shares.Any(s => s.Enabled && s.WebDav),
            _ftp is not null ? FtpPort : 0);
        if (_mdns is null)
            _mdns = MdnsAnnouncer.Start(services, Log);
        else
            _mdns.Update(services);
    }

    private void StartServicesLocked()
    {
        SocketException? failure = null;
        try
        {
            StartHttpLocked();
        }
        catch (SocketException e)
        {
            failure = e;
            Log.Error(Strings.CannotListen(Config.Server.HttpPort, e.Message));
        }
        try
        {
            StartFtpLocked();
        }
        catch (SocketException e)
        {
            failure ??= e;
            Log.Error(Strings.CannotListen(Config.Server.FtpPort, e.Message));
        }
        if (failure is not null)
            throw failure;
    }

    private void StartHttpLocked()
    {
        if (Config.Server.Http && _http is null)
        {
            var web = new WebApp(() => _state, Log, _auth, this);
            var server = new HttpServer(Config.Server.Bind, Config.Server.HttpPort, web.HandleAsync, Log);
            server.Start();
            _http = server;
        }
    }

    private void StartFtpLocked()
    {
        if (Config.Server.Ftp && _ftp is null)
        {
            var server = new FtpServer(() => _state, Log, _auth);
            server.Start();
            _ftp = server;
        }
    }

    private async Task StopHttpLocked()
    {
        if (_http is { } http)
        {
            _http = null;
            await http.DisposeAsync();
        }
    }

    private async Task StopFtpLocked()
    {
        if (_ftp is { } ftp)
        {
            _ftp = null;
            await ftp.DisposeAsync();
        }
    }

    /// <summary>
    /// Aplica una configuracion nueva sin cortar conexiones. Si el servidor esta en marcha y
    /// cambian puertos, direcciones o los interruptores HTTP/FTP, se reinicia el servicio afectado.
    /// Devuelve false (y lo anota en el registro) si no se pudo volver a arrancar.
    /// </summary>
    public async Task<bool> ApplyAsync(SunfishConfig config)
    {
        bool ok = true;
        await _gate.WaitAsync();
        try
        {
            var old = _state.Config.Server;
            _state = new ServerState(config);
            var now = config.Server;
            if (_running)
            {
                bool bind = !old.Bind.SequenceEqual(now.Bind);
                if (bind || old.Http != now.Http || old.HttpPort != now.HttpPort)
                {
                    await StopHttpLocked();
                    ok &= TryStart(StartHttpLocked, now.HttpPort);
                }
                if (bind || old.Ftp != now.Ftp || old.FtpPort != now.FtpPort)
                {
                    await StopFtpLocked();
                    ok &= TryStart(StartFtpLocked, now.FtpPort);
                }
                await UpdateAnnouncerLocked();
            }
        }
        finally
        {
            _gate.Release();
        }
        Changed?.Invoke();
        return ok;
    }

    private bool TryStart(Action start, int port)
    {
        try
        {
            start();
            return true;
        }
        catch (SocketException e)
        {
            Log.Error(Strings.CannotListen(port, e.Message));
            return false;
        }
    }

    /// <summary>Guarda la configuracion en su fichero y la aplica (el vigilante no la recarga dos veces).</summary>
    public async Task<bool> SaveAndApplyAsync(SunfishConfig config)
    {
        if (config.FilePath is not null)
        {
            config.Save();
            _lastWritten = File.ReadAllText(config.FilePath);
        }
        return await ApplyAsync(config);
    }

    /// <summary>
    /// Vigila el fichero de configuracion: si otro programa (un editor, sunfish-cli user...) lo
    /// cambia, se recarga. Si el fichero nuevo tiene errores se aplica igualmente lo valido y se
    /// anotan los errores, como al arrancar.
    /// </summary>
    public void WatchConfigFile()
    {
        string? path = Config.FilePath;
        if (path is null || _watcher is not null)
            return;
        string full = Path.GetFullPath(path);
        _lastWritten = File.Exists(full) ? File.ReadAllText(full) : null;
        _watcher = new FileSystemWatcher(Path.GetDirectoryName(full)!, Path.GetFileName(full))
        {
            NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size,
        };
        _watcher.Changed += (_, _) => ScheduleReload(full);
        _watcher.Created += (_, _) => ScheduleReload(full);
        _watcher.Renamed += (_, _) => ScheduleReload(full);
        _watcher.EnableRaisingEvents = true;
    }

    /// <summary>Los editores escriben en varias tandas: se espera un momento antes de leer.</summary>
    private void ScheduleReload(string path)
    {
        var cts = new CancellationTokenSource();
        Interlocked.Exchange(ref _reloadDelay, cts)?.Cancel();
        _ = Task.Delay(400, cts.Token).ContinueWith(async t =>
        {
            if (t.IsCanceled)
                return;
            try
            {
                string text = File.ReadAllText(path);
                if (text == _lastWritten)
                    return;
                _lastWritten = text;
                var config = SunfishConfig.Parse(text);
                config.FilePath = path;
                Overrides?.Invoke(config);
                foreach (var error in config.Errors)
                    Log.Warning(Strings.ConfigReloadError(error));
                Log.Info(Strings.ConfigReloaded);
                await ApplyAsync(config);
            }
            catch (IOException) { ScheduleReload(path); } // aun bloqueado por quien escribe
            catch (UnauthorizedAccessException) { }
        }, TaskScheduler.Default);
    }

    public async ValueTask DisposeAsync()
    {
        _watcher?.Dispose();
        _watcher = null;
        await StopAsync();
    }
}
