using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Watcher.Data;
using Watcher.Models;
using Watcher.ViewModels;

namespace Watcher.Controllers;

public class HardwareDto
{
    // Server Identity
    [Required]
    public int Id { get; set; }

    [Required]
    public string? IpAddress { get; set; }

    // Hardware Info
    [Required]
    public string? CpuType { get; set; }

    [Required]
    public int CpuCores { get; set; }

    [Required]
    public string? GpuType { get; set; }

    [Required]
    public double RamSize { get; set; }

}

public class MetricDto
{
    // Server Identity
    [Required]
    public int ServerId { get; set; }

    [Required]
    public string? IpAddress { get; set; }

    // Hardware Metrics
    // CPU
    public double CPU_Load { get; set; } // %

    public double CPU_Temp { get; set; } // deg C

    // GPU
    public double GPU_Load { get; set; } // %

    public double GPU_Temp { get; set; } // deg C

    public double GPU_Vram_Size { get; set; } // Bytes

    public double GPU_Vram_Load { get; set; } // %

    // RAM
    public double RAM_Size { get; set; } // Bytes

    public double RAM_Load { get; set; } // %

    // Disks
    public double DISK_Size { get; set; } // Bytes

    public double DISK_Usage { get; set; } // Bytes

    public double DISK_Temp { get; set; } // deg C (if available)

    // Network
    public double NET_In { get; set; } // Bytes/s

    public double NET_Out { get; set; } // Bytes/s
}

public class DockerServiceDto
{
    public int Server_id { get; set; } // Vom Watcher-Server zugewiesene ID des Hosts (optional, falls der Agent diese bereits kennt)
    public string? IpAddress { get; set; } // IP-Adresse des Servers (wird verwendet, falls Server_id nicht gesetzt oder 0 ist)
    public required JsonElement Containers { get; set; }
}

public class DockerServiceMetricDto
{

}

[ApiController]
[Route("[controller]")]
public class MonitoringController : Controller
{
    private readonly AppDbContext _context;

    private readonly ILogger<MonitoringController> _logger;

    public MonitoringController(AppDbContext context, ILogger<MonitoringController> logger)
    {
        _context = context;
        _logger = logger;
    }


    // Endpoint, an den der Agent seine Hardwareinformationen schickt
    [HttpPost("hardware-info")]
    public async Task<IActionResult> Register([FromBody] HardwareDto dto)
    {
        // Gültigkeit des Payloads prüfen
        if (!ModelState.IsValid)
        {
            var errors = ModelState.Values
                .SelectMany(v => v.Errors)
                .Select(e => e.ErrorMessage)
                .ToList();

            _logger.LogError("Fehlerhafter Registrierungs-Payload.");
            return BadRequest(new { error = "Ungültiger Payload", details = errors });
        }

        // Server in Datenbank finden
        var server = await _context.Servers
            .FirstOrDefaultAsync(s => s.IPAddress == dto.IpAddress);

        if (server != null)
        {
            // Serverdaten in Datenbank eintragen
            server.CpuType = dto.CpuType;
            server.CpuCores = dto.CpuCores;
            server.GpuType = dto.GpuType;
            server.RamSize = dto.RamSize;

            // Änderungen in Datenbank speichern
            await _context.SaveChangesAsync();

            // Success - Server-ID und IP-Adresse zurückgeben
            _logger.LogInformation("Agent für '{server}' erfolgreich registriert.", server.Name);
            return Ok(new
            {
                id = server.Id,
                ipAddress = server.IPAddress
            });
        }
        _logger.LogError("Kein Server für Registrierung gefunden");
        return NotFound("No Matching Server found.");
    }

    // Endpoint, an dem sich ein Agent initial registriert
    [HttpGet("register")]
    public async Task<IActionResult> GetServerIdByIp([FromQuery] string IpAddress)
    {
        var server = await _context.Servers
            .FirstOrDefaultAsync(s => s.IPAddress == IpAddress);

        if (server == null)
            return NotFound();

        return Ok(new
        {
            id = server.Id,
            IpAddress = server.IPAddress
        });
    }


    // Enpoint, an den Agents Ihre gesammelten Daten senden
    [HttpPost("metric")]
    public async Task<IActionResult> Receive([FromBody] MetricDto dto)
    {
        // Gültigkeit des Payloads prüfen
        if (!ModelState.IsValid)
        {
            var errors = ModelState.Values
                .SelectMany(v => v.Errors)
                .Select(e => e.ErrorMessage)
                .ToList();

            _logger.LogError("Ungültiger Monitoring-Payload.");
            return BadRequest(new { error = "Ungültiger Payload", details = errors });
        }

        // Debug-Logging
        _logger.LogDebug("Metric Request empfangen: ServerId={ServerId}, IpAddress={IpAddress}",
                         dto.ServerId, dto.IpAddress ?? "null");

        // Server in Datenbank finden (priorisiert IP-Adresse, dann ID)
        _logger.LogInformation("ServerID:  {ServerId}", dto.ServerId);
        var server = await FindServerByIpOrId(dto.ServerId);

        if (server == null)
        {
            _logger.LogError("Kein Server mit IP '{IpAddress}' oder ID {ServerId} gefunden",
                           dto.IpAddress, dto.ServerId);
            return NotFound(new {
                error = "Server not found",
                details = $"Server mit ID {dto.ServerId} existiert nicht. Bitte zuerst registrieren."
            });
        }

        // neues Metric-Objekt erstellen
        var newMetric = new Metric
        {
            Timestamp = DateTime.UtcNow,
            ServerId = server.Id, // Verwende die tatsächliche Server-ID aus der DB
            CPU_Load = SanitizeInput(dto.CPU_Load),
            CPU_Temp = SanitizeInput(dto.CPU_Temp),
            GPU_Load = SanitizeInput(dto.GPU_Load),
            GPU_Temp = SanitizeInput(dto.GPU_Temp),
            GPU_Vram_Size = CalculateGigabyte(dto.GPU_Vram_Size),
            GPU_Vram_Usage = SanitizeInput(dto.GPU_Vram_Load),
            RAM_Load = SanitizeInput(dto.RAM_Load),
            RAM_Size = CalculateGigabyte(dto.RAM_Size),
            DISK_Size = CalculateGigabyte(dto.DISK_Size),
            DISK_Usage = CalculateGigabyte(dto.DISK_Usage),
            DISK_Temp = SanitizeInput(dto.DISK_Temp),
            NET_In = CalculateMegabit(dto.NET_In),
            NET_Out = CalculateMegabit(dto.NET_Out)
        };

        try
        {
            // Metric Objekt in Datenbank einfügen
            _context.Metrics.Add(newMetric);
            await _context.SaveChangesAsync();

            _logger.LogInformation("Monitoring-Daten für Server '{ServerName}' (ID: {ServerId}) in Datenbank geschrieben",
                                 server.Name, server.Id);
            return Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Metric für Server '{ServerName}' (ID: {ServerId}) konnte nicht gespeichert werden",
                           server.Name, server.Id);
            return StatusCode(500, new { error = "Database error", details = ex.Message });
        }
    }

    // Endpoint, an dem Agents Ihre laufenden Services registrieren
    [HttpPost("service-discovery")]
    public async Task<IActionResult> ServiceDetection([FromBody] DockerServiceDto dto)
    {
        // Gültigkeit des Payloads prüfen
        if (!ModelState.IsValid)
        {
            var errors = ModelState.Values
                .SelectMany(v => v.Errors)
                .Select(e => e.ErrorMessage)
                .ToList();

            _logger.LogError("Invalid ServiceDetection-Payload.");
            return BadRequest(new { error = "Invalid Payload", details = errors });
        }

        // Debug-Logging für eingehende Requests
        _logger.LogDebug("Service-Discovery Request empfangen: Server_id={ServerId}, IpAddress={IpAddress}",
                         dto.Server_id, dto.IpAddress ?? "null");

        // Server in Datenbank finden (priorisiert IP-Adresse, dann ID)
        var server = await FindServerByIpOrId(dto.Server_id);

        if (server == null)
        {
            _logger.LogError("Server with IP '{IpAddress}' or ID {ServerId} does not exist.",
                           dto.IpAddress, dto.Server_id);
            return BadRequest(new {
                error = "Server not found",
                details = $"Server with IP '{dto.IpAddress}' or ID {dto.Server_id} does not exist. Please register the server first."
            });
        }

        // Server ID für die weitere Verarbeitung setzen
        int serverId = server.Id;

        List<Container> newContainers =
            JsonSerializer.Deserialize<List<Container>>(dto.Containers.GetRawText())
            ?? new List<Container>();

        foreach (Container container in newContainers)
        {
            container.ServerId = serverId;
            // Debug Logs
            // TODO entfernen wenn fertig getestet
            Console.WriteLine("---------");
            Console.WriteLine("ServerId: " + container.ServerId);
            Console.WriteLine("ContainerId: " + container.ContainerId);
            Console.WriteLine("Name: " + container.Name);
            Console.WriteLine("Image: " + container.Image);
            Console.WriteLine("---------");

        }

        // Liste aller Container, die bereits der übergebenen ServerId zugewiesen sind
        List<Container> existingContainers = _context.Containers
            .Where(c => c.ServerId == serverId)
            .ToList();


        // Logik, um Container, die mit dem Payload kamen zu verarbeiten
        foreach (Container container in newContainers)
        {
            // Überprüfen, ob ein übergebener Container bereits für den Host registriert ist
            // Wichtig: Vergleich über ContainerId, nicht über Objektreferenz!
            var existingContainer = existingContainers
                .FirstOrDefault(ec => ec.ContainerId == container.ContainerId);

            if (existingContainer != null)
            {
                // Container existiert bereits, nur Daten aktualisieren falls sich etwas geändert hat
                existingContainer.Name = container.Name;
                existingContainer.Image = container.Image;
                existingContainer.IsRunning = true;

                _logger.LogInformation("Container '{containerName}' (ID: {containerId}) already exists for Server {serverId}, updated.", container.Name, container.ContainerId, serverId);
            }
            // Container auf einen Host/Server registrieren
            else
            {
                // Container in Datenbank einlesen
                try
                {
                    _context.Containers.Add(container);
                    await _context.SaveChangesAsync();
                    _logger.LogInformation("Container '{containerName}' (ID: {containerId}) added for Server {serverId}", container.Name, container.ContainerId, container.ServerId);
                }
                catch (SqliteException e)
                {
                    _logger.LogError("Error writing new Container '{containerName}' to Database: {error}", container.Name, e.Message);
                }
            }
        }

        // Logik um abgeschaltene Container aus der Datenbank zu entfernen
        foreach (Container existingContainer in existingContainers)
        {
            // Abfrage, ob bereits vorhandener Container im Payload vorhanden war
            // Wichtig: Vergleich über ContainerId, nicht über Objektreferenz!
            var stillRunning = newContainers
                .Any(nc => nc.ContainerId == existingContainer.ContainerId);

            if (!stillRunning)
            {
                // Container entfernen
                _context.Containers.Remove(existingContainer);
                await _context.SaveChangesAsync();

                // Metrics für den Container entfernen
                //Todo

                _logger.LogInformation("Container '{containerName}' (DB-ID: {id}, ContainerID: {containerId}) on Server {serverId} was removed from the database.", existingContainer.Name, existingContainer.Id, existingContainer.ContainerId, existingContainer.ServerId);
            }
        }

        // Alle Änderungen in einem Batch speichern
        await _context.SaveChangesAsync();

        return Ok();

    }

    // Endpoint, an den der Agent die Metrics der registrierten Container schickt
    public async Task<IActionResult> ServiceMetrics([FromBody] DockerServiceMetricDto dto)
    {
        // Gültigkeit des Payloads prüfen
        if (!ModelState.IsValid)
        {
            var errors = ModelState.Values
                .SelectMany(v => v.Errors)
                .Select(e => e.ErrorMessage)
                .ToList();

            _logger.LogError("Invalid ServiceDetection-Payload.");
            return BadRequest(new { error = "Invalid Payload", details = errors });
        }

        // Liste an Metrics aus der dto erstellen
        List<ContainerMetric> metrics = new List<ContainerMetric>();

        // Metrics in die Datenbank eintragen
        try
        {
            foreach (ContainerMetric m in metrics)
            {
                _context.ContainerMetrics.Add(m);
                await _context.SaveChangesAsync();
                // _logger.LogInformation(m. + " added for Host " + c.ServerId);
                return Ok();
            }
        }
        catch (SqliteException e)
        {
            _logger.LogError(e.Message);
            return StatusCode(500);
        }

        return Ok();

    }

    // Durchschnittliche Werte Berechnen
    public async Task<IActionResult> CalculateMedian(string Metric, int HoursToMonitor, int ServerId)
    {
        // Aktuelle Zeit - X Stunden = letzter Wert, der berücksichtigt werden soll
        DateTime TimeToMonitor = DateTime.Now.AddHours(-HoursToMonitor);

        // Alle Metrics von Server "ServerId" finden, die in der festgelegten Zeitspanne liegen
        var MetricsInTimeFrame = await _context.Metrics
            .Where(e => e.Timestamp >= TimeToMonitor && e.ServerId == ServerId)
            .ToListAsync();

        // return Action (MetricsInTimeFrame)

        return NotFound();
    }

    [Authorize]
    [HttpGet("cpu-usage")]
    public async Task<IActionResult> GetCpuUsageData(int serverId, int hours = 1)
    {
        var startTime = DateTime.UtcNow.AddHours(-hours);
        var metrics = await _context.Metrics
                                .Where(m => m.Timestamp >= startTime && m.ServerId == serverId)
                                .OrderBy(m => m.Timestamp)
                                .ToListAsync();

        // Timestamp-Format basierend auf Zeitbereich anpassen
        string format = hours > 1 ? "dd.MM HH:mm" : "HH:mm";
        var data = metrics.Select(m => new
        {
            label = m.Timestamp.ToLocalTime().ToString(format),
            data = m.CPU_Load
        }).ToList();

        return Ok(data);
    }

    [Authorize]
    [HttpGet("ram-usage")]
    public async Task<IActionResult> GetRamUsageData(int serverId, int hours = 1)
    {
        var startTime = DateTime.UtcNow.AddHours(-hours);
        var metrics = await _context.Metrics
                                .Where(m => m.Timestamp >= startTime && m.ServerId == serverId)
                                .OrderBy(m => m.Timestamp)
                                .ToListAsync();

        // Timestamp-Format basierend auf Zeitbereich anpassen
        string format = hours > 1 ? "dd.MM HH:mm" : "HH:mm";
        var data = metrics.Select(m => new
        {
            label = m.Timestamp.ToLocalTime().ToString(format),
            data = m.RAM_Load
        }).ToList();

        return Ok(data);
    }

    [Authorize]
    [HttpGet("gpu-usage")]
    public async Task<IActionResult> GetGpuUsageData(int serverId, int hours = 1)
    {
        var startTime = DateTime.UtcNow.AddHours(-hours);
        var metrics = await _context.Metrics
                                .Where(m => m.Timestamp >= startTime && m.ServerId == serverId)
                                .OrderBy(m => m.Timestamp)
                                .ToListAsync();

        // Timestamp-Format basierend auf Zeitbereich anpassen
        string format = hours > 1 ? "dd.MM HH:mm" : "HH:mm";
        var data = metrics.Select(m => new
        {
            label = m.Timestamp.ToLocalTime().ToString(format),
            data = m.GPU_Load
        }).ToList();

        return Ok(data);
    }

    [Authorize]
    [HttpGet("current-metrics/{serverId}")]
    public async Task<IActionResult> GetCurrentMetrics(int serverId)
    {
        var latestMetric = await _context.Metrics
            .Where(m => m.ServerId == serverId)
            .OrderByDescending(m => m.Timestamp)
            .FirstOrDefaultAsync();

        if (latestMetric == null)
        {
            return Ok(new
            {
                cpu = 0.0,
                ram = 0.0,
                gpu = 0.0,
                hasData = false
            });
        }

        return Ok(new
        {
            cpu = latestMetric.CPU_Load,
            ram = latestMetric.RAM_Load,
            gpu = latestMetric.GPU_Load,
            hasData = true
        });
    }

    // Metric Input Byte zu Gigabyte umwandeln
    public static double CalculateGigabyte(double metricInput)
    {
        // *10^-9 um auf Gigabyte zu kommen
        double calculatedValue = metricInput * Math.Pow(10, -9);

        // Auf 2 Nachkommastellen runden
        double calculatedValueSanitized = SanitizeInput(calculatedValue);

        return calculatedValueSanitized;
    }

    // Metric Input Byte/s zu Megabit/s umrechnen
    //TODO
    public static double CalculateMegabit(double metricInput)
    {
        // *10^-9 um auf Gigabyte zu kommen
        double calculatedValue = metricInput * Math.Pow(10, -9);

        // Auf 2 Nachkommastellen runden
        double calculatedValueSanitized = SanitizeInput(calculatedValue);

        return calculatedValueSanitized;
    }

    // Degree Input auf zwei Nachkommastellen runden
    public static double SanitizeInput(double metricInput)
    {
        return Math.Round(metricInput, 2);
    }

    // ==================== HELPER METHODS ====================

    /// <summary>
    /// Findet einen Server anhand IP-Adresse oder ID.
    /// Priorisiert IP-Adresse, fällt dann auf ID zurück.
    /// </summary>
    /// <param name="ipAddress">IP-Adresse des Servers (bevorzugt)</param>
    /// <param name="serverId">Server-ID (Fallback)</param>
    /// <returns>Server-Objekt oder null wenn nicht gefunden</returns>
    private async Task<Server> FindServerByIpOrId(int serverId)
    {
        // WICHTIG: >= 0 da Server-IDs bei 0 starten können
        if (serverId >= 0)
        {
            var server = await _context.Servers
                .FirstOrDefaultAsync(s => s.Id == serverId);

            if (server != null)
            {
                _logger.LogDebug("Server gefunden via ID: {ServerName}", server.Name);
                return server;
            } else {
                // Server nicht gefunden
                _logger.LogWarning("Server nicht gefunden: ID={ServerId}", serverId);
                return null;
            }
        } else {
            _logger.LogWarning("Ungültige ID abgefragt!");
            return null;
        }

        
    }

    private List<Container> ParseServiceDiscoveryInput(int serverId, List<Container> containers)
    {
        List<Container> containerList = new List<Container>();

        // JSON-Objekt auslesen und Container-Objekte erstellen


        return containerList;
    }
}