using ClawdDotNet.App.Services;
using ClawdDotNet.App.Settings;
using ClawdDotNet.Core.Accounting;
using ClawdDotNet.Core.Api;
using ClawdDotNet.Core.Audit;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Logging;
using ClawdDotNet.Core.Memory;
using ClawdDotNet.Core.Security;
using ClawdDotNet.Core.Staging;
using ClawdDotNet.Core.State;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using ClawdDotNet.Core.Deploymentcenter;
using ClawdDotNet.Core.Deploymentcenter.Watchdog;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Tools.AgentComm;
using ClawdDotNet.Tools.AgentEditor;
using ClawdDotNet.Tools.AgentSpawn;
using ClawdDotNet.Tools.Database;
using ClawdDotNet.Tools.DirectAPI;
using ClawdDotNet.Tools.FileRW;
using ClawdDotNet.Tools.FTP;
using ClawdDotNet.Tools.Mail;
using ClawdDotNet.Tools.SocialMediaManager;
using ClawdDotNet.Tools.Telegram;
using ClawdDotNet.Tools.TelegramClient;
using ClawdDotNet.Tools.WebFetch;
using ClawdDotNet.Tools.WebMonitor;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.App;

/// <summary>
/// Baut alles auf, was ClawdDotNet zum Laufen braucht — <b>ohne</b> eine einzige Zeile
/// Oberflächencode.
///
/// <para>Vorher lag das in <c>Program.cs</c> der WinForms-Anwendung: 350 Zeilen zwischen
/// <c>ApplicationConfiguration.Initialize()</c> und <c>Application.Run(form)</c>. Die
/// Trennung bestand faktisch schon — alles war fertig aufgebaut, bevor das Fenster
/// überhaupt entstand. Sie war nur nirgends festgehalten.</para>
///
/// <para>Damit setzen zwei Aufrufer auf demselben Aufbau auf: die Avalonia-Anwendung
/// und der geplante systemd-Dienst. Was ein Fenster braucht — Instanzauswahl,
/// Lizenzabfrage, Telegram-Anmeldung — kommt als Rückruf herein, statt hier
/// festgeschrieben zu sein.</para>
/// </summary>
public sealed class AppHost : IAsyncDisposable
{
    private readonly List<Func<ValueTask>> _shutdown = [];

    public required SettingsManager Settings { get; init; }
    public required InstanceDirectoryManager Directories { get; init; }
    public required InstanceConfig Instance { get; init; }
    public required string InstancePath { get; init; }
    public required string LogDirectory { get; init; }
    public required ILoggerFactory LoggerFactory { get; init; }
    public required ToolRegistry Tools { get; init; }

    /// <summary>Null, wenn kein OpenRouter-Schlüssel hinterlegt ist — dann laufen keine Agenten.</summary>
    public AgentEngine? Engine { get; private init; }

    public TaskScanner? Scanner { get; private init; }
    public StagingService? Staging { get; private init; }
    public SqliteUsageRepository? Usage { get; private init; }
    public TelegramClientManager? TelegramClient { get; private init; }
    public OpenRouterStatusService? Status { get; private init; }

    /// <summary>
    /// Die taegliche Sicherung. Immer gesetzt — ob sie etwas tut, entscheidet
    /// <c>AutoBackupEnabled</c> bei jedem Durchlauf neu, damit eine Aenderung in den
    /// Einstellungen ohne Neustart greift.
    /// </summary>
    public BackupScheduler? Backups { get; private set; }

    /// <summary>
    /// Anbindung ans Deploymentcenter (Heartbeat, Fehler-Stream, Bugtracker, Updates).
    /// Null, wenn Adresse oder Token fehlen.
    /// </summary>
    public DeploymentcenterService? Deploymentcenter { get; private set; }

    /// <summary>
    /// Meldeweg für ungefangene Ausnahmen. Immer gesetzt — ohne Anbindung ist es der
    /// Leerlauf, damit Aufrufer nicht auf null prüfen müssen.
    /// </summary>
    public IErrorReporter Errors => Deploymentcenter?.Errors ?? NullErrorReporter.Instance;

    /// <summary>
    /// Der Lizenz-Torwächter bleibt über die Laufzeit erhalten: Ein Widerruf soll auch
    /// eine bereits laufende Instanz erreichen, nicht erst den nächsten Start.
    /// </summary>
    public LicenseGate? License { get; private set; }

    /// <summary>
    /// Die laufende Nachprüfung. Der Aufrufer hängt sich an
    /// <see cref="LicenseWatch.Revoked"/> und beendet die Anwendung, wenn es feuert.
    /// </summary>
    public LicenseWatch? LicenseWatch { get; private set; }

    /// <summary>Was der Aufrufer beim Start erfragen muss.</summary>
    public sealed class Callbacks
    {
        /// <summary>
        /// Wählt die Instanz. Gibt <c>null</c> zurück, wenn der Nutzer abbricht.
        /// Ein Dienst liefert hier den fest eingestellten Pfad, ohne zu fragen.
        /// </summary>
        public required Func<InstanceDirectoryManager, Task<string?>> SelectInstance { get; init; }

        /// <summary>Wie der Lizenz-Torwächter mit dem Benutzer spricht.</summary>
        public required ILicensePrompt License { get; init; }

        /// <summary>
        /// Telegram-Anmeldecode und 2FA-Passwort. Null lässt die MTProto-Anmeldung aus —
        /// im kopflosen Betrieb der richtige Weg, weil ein Eingabefenster dort einen
        /// Dienst dauerhaft blockieren würde.
        /// </summary>
        public Func<string, Task<string>>? TelegramLogin { get; init; }

        public Func<Task<string>>? Telegram2FA { get; init; }
    }

    /// <summary>
    /// Ergebnis des Aufbaus. <see cref="Host"/> ist null, wenn der Nutzer abgebrochen hat
    /// oder die Lizenz fehlt — der Aufrufer beendet dann, ohne eine Fehlermeldung
    /// nachzureichen: Die hat der Torwächter schon gezeigt.
    /// </summary>
    public readonly record struct StartupResult(AppHost? Host, string? Error);

    public static async Task<StartupResult> StartAsync(Callbacks callbacks, CancellationToken ct = default)
    {
        // ─── 1. Anwendungseinstellungen ───
        var settings = new SettingsManager();
        settings.Load();

        // ─── 2. Instanz wählen ───
        var directories = new InstanceDirectoryManager(
            Path.GetFullPath(settings.AppSettings.InstancesDirectory));

        var instancePath = await callbacks.SelectInstance(directories);
        if (string.IsNullOrWhiteSpace(instancePath))
            return new StartupResult(null, null); // Abbruch, keine Meldung nötig

        InstanceConfig instance;
        try
        {
            instance = directories.LoadInstanceConfig(instancePath);
        }
        catch (Exception ex)
        {
            return new StartupResult(null,
                $"Fehler beim Laden der Instanz:\n{instancePath}\n\n{ex.Message}");
        }

        // ─── 3. Protokollierung ───
        var logDirectory = Path.GetFullPath(
            !string.IsNullOrWhiteSpace(instance.LogDirectory)
                ? instance.LogDirectory
                : settings.AppSettings.LogDirectory);

        var minLevel = Enum.TryParse<Core.Logging.LogLevel>(
            settings.AppSettings.MinimumLogLevel, true, out var parsed)
            ? parsed
            : Core.Logging.LogLevel.Info;

        var loggerFactory = LoggingExtensions.CreateClawdLoggerFactory(logDirectory, minLevel);
        var logger = loggerFactory.CreateLogger("ClawdDotNet.Startup");

        logger.LogInformation("ClawdDotNet startet – Instanz: {Instance} ({Id})",
            instance.InstanceName, instance.InstanceId);
        logger.LogInformation("Instanz-Verzeichnis: {Path}", instancePath);
        logger.LogInformation("Einstellungen: {Path}", settings.SettingsPath);

        // ─── 4. Lizenz ───
        var license = new LicenseGate(settings, loggerFactory.CreateLogger("ClawdDotNet.License"),
            callbacks.License);

        if (!await license.RunStartupCheckAsync(ct))
        {
            logger.LogWarning("Start abgebrochen: keine gültige Lizenz.");
            loggerFactory.Dispose();
            return new StartupResult(null, null);
        }

        // ─── 5. Werkzeuge ───
        var tools = RegisterTools();

        TelegramClientManager? telegram = null;
        if (instance.TelegramClient is not null && callbacks.TelegramLogin is not null)
        {
            telegram = new TelegramClientManager(instance, instancePath,
                loggerFactory.CreateLogger("ClawdDotNet.Tools.TelegramClient"));

            tools.Register(new TelegramClientTool(telegram));
            logger.LogInformation("TelegramClient-Tool registriert");
        }
        else if (instance.TelegramClient is not null)
        {
            logger.LogInformation(
                "TelegramClient konfiguriert, aber kein Anmeldeweg vorhanden – übersprungen.");
        }

        var host = BuildCore(settings, directories, instance, instancePath,
            logDirectory, loggerFactory, tools, telegram, logger);

        // Nichts freizugeben: Der Torwächter nutzt den gemeinsamen HttpClient des SDK.
        host.License = license;

        if (license.IsEnforcementConfigured)
        {
            host.LicenseWatch = new LicenseWatch(
                license, loggerFactory.CreateLogger("ClawdDotNet.License"));

            host.LicenseWatch.Start();
            host._shutdown.Add(host.LicenseWatch.DisposeAsync);
        }

        await host.ConnectTelegramAsync(callbacks, logger);
        await host.StartDeploymentcenterAsync(loggerFactory, ct);

        // Die Oberflaeche bietet die taegliche Sicherung an und schreibt Uhrzeit und
        // Zielordner in die Einstellungen — gestartet wurde der Takt dazu bisher nicht,
        // die Einstellung lief also ins Leere.
        host.Backups = new BackupScheduler(
            instancePath, instance.InstanceName, settings,
            loggerFactory.CreateLogger("ClawdDotNet.Backup"));

        host.Backups.Start();
        host._shutdown.Add(host.Backups.DisposeAsync);

        return new StartupResult(host, null);
    }

    // ─── Aufbau ───

    private static ToolRegistry RegisterTools()
    {
        var registry = new ToolRegistry();

        registry.Register(new FileRWTool());
        registry.Register(new TelegramTool());
        registry.Register(new MailTool());
        registry.Register(new DatabaseTool());
        registry.Register(new FTPTool());
        registry.Register(new DirectApiTool());
        registry.Register(new WebFetchTool());
        registry.Register(new WebMonitorTool());
        registry.Register(new AgentCommTool());
        registry.Register(new SocialMediaManagerTool());
        registry.Register(new AgentSpawnTool());
        registry.Register(new AgentEditorTool());
        registry.Register(new Tools.Memory.MemoryTool());
        registry.Register(new Tools.Taskboard.TaskboardTool());
        registry.Register(new Tools.RocketChat.RocketChatTool());

        return registry;
    }

    private static AppHost BuildCore(
        SettingsManager settings,
        InstanceDirectoryManager directories,
        InstanceConfig instance,
        string instancePath,
        string logDirectory,
        ILoggerFactory loggerFactory,
        ToolRegistry tools,
        TelegramClientManager? telegram,
        ILogger logger)
    {
        if (string.IsNullOrWhiteSpace(instance.OpenRouterApiKey))
        {
            logger.LogWarning("Kein OpenRouter API-Key konfiguriert – Agenten sind deaktiviert");

            return new AppHost
            {
                Settings = settings,
                Directories = directories,
                Instance = instance,
                InstancePath = instancePath,
                LogDirectory = logDirectory,
                LoggerFactory = loggerFactory,
                Tools = tools,
                TelegramClient = telegram
            };
        }

        var openRouter = new OpenRouterClient(instance.OpenRouterApiKey,
            loggerFactory.CreateLogger("ClawdDotNet.Core.Api.OpenRouterClient"));

        // Eine Datenbank je Instanz; StateStore, Gedächtnis und Taskboard teilen sie sich.
        var storage = new SqliteStorage(Path.Combine(instancePath, "state.db"));
        var stateStore = new SqliteStateStore(storage);
        var memory = new SqliteMemoryRepository(storage);
        var taskRepository = new SqliteTaskRepository(storage);
        var audit = new SqliteAuditRepository(storage);
        var stagingRepository = new SqliteStagingRepository(storage);
        var stagingGate = new StagingGate(new StagingPolicy(), stagingRepository);
        var usage = new SqliteUsageRepository(storage);

        // Preise fürs Budget: Ohne sie greift nur die Token-Grenze.
        var pricing = new ModelPricingCatalog();
        _ = Task.Run(async () =>
        {
            try { pricing.Load(await openRouter.GetAvailableModelsAsync()); }
            catch { /* Ohne Preise bleibt die Kostengrenze wirkungslos, die Token-Grenze nicht. */ }
        });

        var engine = new AgentEngine(
            openRouter, tools, new PermissionGate(), stateStore, loggerFactory,
            memory, usage, pricing, taskRepository, audit, stagingGate)
        {
            InstanceBudget = instance.Budget
        };

        engine.SetAgentConfigProvider(
            () => instance.Agents,
            instance.InstanceId,
            agentId =>
            {
                var agent = instance.Agents.FirstOrDefault(a => a.AgentId == agentId);
                return string.IsNullOrWhiteSpace(agent?.AgentDir) ? null : agent.AgentDir;
            });

        engine.LoadPersistedChats();

        var (scanner, staging) = BuildTaskboard(
            instance, taskRepository, engine, tools, stateStore, audit,
            stagingRepository, loggerFactory, logger);

        var host = new AppHost
        {
            Settings = settings,
            Directories = directories,
            Instance = instance,
            InstancePath = instancePath,
            LogDirectory = logDirectory,
            LoggerFactory = loggerFactory,
            Tools = tools,
            Engine = engine,
            Scanner = scanner,
            Staging = staging,
            Usage = usage,
            TelegramClient = telegram,
            Status = new OpenRouterStatusService(instance.OpenRouterApiKey)
        };

        host._shutdown.Add(() => { openRouter.Dispose(); return ValueTask.CompletedTask; });

        logger.LogInformation("AgentEngine und Scanner erstellt, Chat-Verläufe geladen");

        return host;
    }

    /// <summary>
    /// Taskboard und Scanner. Der Scanner ist der einzige periodische Treiber (A1) —
    /// geplante Agentenläufe wie Tool-Job-Polls sind Tasks.
    /// </summary>
    private static (TaskScanner?, StagingService?) BuildTaskboard(
        InstanceConfig instance,
        SqliteTaskRepository taskRepository,
        AgentEngine engine,
        ToolRegistry tools,
        SqliteStateStore stateStore,
        SqliteAuditRepository audit,
        SqliteStagingRepository stagingRepository,
        ILoggerFactory loggerFactory,
        ILogger logger)
    {
        var sharedWorkspace = instance.Agents
            .Select(a => a.SharedWorkspacePath)
            .FirstOrDefault(p => !string.IsNullOrWhiteSpace(p));

        if (string.IsNullOrWhiteSpace(sharedWorkspace))
            return (null, null);

        var board = new TaskboardService(taskRepository, Path.Combine(sharedWorkspace, "tasks"));

        var staging = new StagingService(stagingRepository, engine, board, loggerFactory, audit);

        var dispatcher = new EngineTaskDispatcher(
            engine, () => instance.Agents, instance.InstanceId, tools, stateStore, loggerFactory);

        var scanner = new TaskScanner(taskRepository, dispatcher, loggerFactory);

        // Reconciliation nicht blockierend: Der Start soll nicht auf das Dateisystem warten.
        _ = Task.Run(async () =>
        {
            try
            {
                var reset = await taskRepository.ReleaseStaleClaimsAsync(
                    DateTime.UtcNow.AddMinutes(-15), DateTime.UtcNow, CancellationToken.None);

                var coordination = new CoordinationMigration(
                    board, Path.Combine(sharedWorkspace, "coordination"), loggerFactory);
                var migratedCoordination = await coordination.RunAsync(CancellationToken.None);

                var scheduler = new SchedulerTaskMigration(board, taskRepository, loggerFactory);
                var migratedScheduler = await scheduler.RunAsync(instance.Agents, CancellationToken.None);

                var imported = await board.ImportAllAsync(CancellationToken.None);

                logger.LogInformation(
                    "Taskboard bereit: {Imported} Aufgabe(n), migriert {Coord} coordination + "
                    + "{Sched} scheduler, {Reset} verwaiste Claims zurückgesetzt",
                    imported, migratedCoordination, migratedScheduler, reset);

                scanner.Start();
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Taskboard-Reconciliation beim Start fehlgeschlagen");
            }
        });

        return (scanner, staging);
    }

    private async Task ConnectTelegramAsync(Callbacks callbacks, ILogger logger)
    {
        if (TelegramClient is null || callbacks.TelegramLogin is null) return;

        TelegramClient.OnLoginCodeRequired += prompt => callbacks.TelegramLogin(prompt);

        if (callbacks.Telegram2FA is not null)
            TelegramClient.On2FAPasswordRequired += () => callbacks.Telegram2FA();

        try
        {
            await TelegramClient.ConnectAsync(CancellationToken.None);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Telegram: Login fehlgeschlagen");
        }
    }

    /// <summary>
    /// Anbindung ans Deploymentcenter: Instanz-Heartbeat, Fehler-Stream, Bugtracker und
    /// die einmalige Update-Prüfung.
    ///
    /// <para>Jede laufende Instanz meldet sich als eigener Monitor — der Server führt
    /// sie über <c>source</c> + <c>instance</c>. Stürzt eine von mehreren ab, fällt
    /// genau deren Monitor, und der Evaluator schlägt nur dafür Alarm.</para>
    /// </summary>
    private async Task StartDeploymentcenterAsync(ILoggerFactory loggerFactory, CancellationToken ct)
    {
        Deploymentcenter = DeploymentcenterService.TryCreate(
            Settings.AppSettings, AppVersion, loggerFactory);

        if (Deploymentcenter is null)
            return;

        _shutdown.Add(Deploymentcenter.DisposeAsync);

        var health = new InstanceHealthProvider(
            Instance.InstanceName,
            agentsEnabled: Engine is not null,
            Instance.Budget,
            Usage,
            () => Instance.Agents.Count,
            () => Engine?.RunningChatCount ?? 0,
            // „Noch nicht gestartet" ist kein Fehler: Der Scanner läuft erst nach der
            // Startabgleichung los, der erste Heartbeat geht sofort raus.
            schedulerRunning: Scanner is null ? null : () => !Scanner.HasStopped);

        await Deploymentcenter.StartWatchdogAsync(
            Instance, health,
            saveInstanceConfig: () => Directories.SaveInstanceConfig(InstancePath, Instance),
            ct);

        // Nicht abwarten: Ein langsamer oder stummer Server darf den Start nicht aufhalten.
        _ = Deploymentcenter.CheckForUpdateAsync(Settings.AppSettings, AppVersion, CancellationToken.None);
    }

    /// <summary>
    /// Die Version, die nach draußen geht: Aktivierungsliste, Heartbeat,
    /// Fehlermeldungen, Versionsvergleich. Kommt aus <c>&lt;Version&gt;</c> in
    /// <c>Directory.Build.props</c> und wird zur Übersetzungszeit eingebettet
    /// (<c>Deploymentcenter.BuildInfo.targets</c>) — zusammen mit Commit und Build-Datum.
    ///
    /// <para>Nicht zu verwechseln mit <c>ClawdDotNet.Core.BuildInfo.Build</c>: das ist
    /// ein von Hand geführter Zähler mit Änderungstext, keine Versionsangabe.</para>
    /// </summary>
    public static string AppVersion => ReleaseInfo.Version;

    /// <summary>Version, Commit, Build-Datum und Kanal in einer Zeile — für Anzeigen.</summary>
    public static string BuildSummary => ReleaseInfo.Summary;

    private bool _disposed;

    public async ValueTask DisposeAsync()
    {
        // Beim Update fahren wir vor dem Beenden selbst herunter; danach ruft die
        // Oberflaeche Shutdown, und deren Behandlung raeumt ein zweites Mal auf. Ohne
        // diese Sperre gingen alle Schritte doppelt los — unter anderem ein zweiter
        // Abmeldevorgang beim Watchdog, der den gerade gesetzten Wartungszustand
        // wieder ueberschreibt.
        if (_disposed) return;
        _disposed = true;

        foreach (var step in _shutdown)
        {
            try { await step(); }
            catch { /* Beim Beenden zaehlt, dass alle Schritte drankommen */ }
        }

        if (Status is not null) await Status.DisposeAsync();
        if (Scanner is not null) await Scanner.DisposeAsync();
        if (TelegramClient is not null) await TelegramClient.DisposeAsync();

        LoggerFactory.Dispose();
    }
}
