using System.Diagnostics;
using System.ComponentModel;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text.Json;
using System.Text.Json.Serialization;
using Lumi.Companion.Protocol;

namespace Lumi.Companion.App;

public sealed class ObsBridgeManager
{
    private readonly string _installRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "obs-studio", "plugins", "lumi-obs-bridge");
    private string? _packageFailure;
    private string InstalledDll => Path.Combine(_installRoot, "bin", "64bit", "lumi-obs-bridge.dll");
    private string InstalledManifest => Path.Combine(_installRoot, "lumi-managed.json");

    public ObsBridgeStatus Inspect()
    {
        var package = LoadPackage();
        var installed = ReadManifest(InstalledManifest);
        var packageAvailable = package is not null;
        var fileInstalled = File.Exists(InstalledDll);
        var valid = packageAvailable && fileInstalled && HashFile(InstalledDll) == package!.Manifest.Sha256 && installed?.Version == package.Manifest.Version;
        return new ObsBridgeStatus(packageAvailable, fileInstalled, valid, package?.Manifest.Version,
            valid ? $"OBS integration {package!.Manifest.Version} is installed. Restart OBS if it was open during the last repair." :
            !packageAvailable ? $"The bundled OBS integration could not be verified. {_packageFailure ?? "The packaged component was not found."}" :
            fileInstalled ? "The OBS integration is outdated or damaged. Repair it while OBS is closed." : "The OBS integration is ready to install.");
    }

    public async Task<ObsBridgeStatus> InstallOrRepairAsync(CancellationToken cancellationToken = default)
    {
        EnsureObsClosed();
        var status = Inspect();
        if (!status.PackageAvailable) throw new InvalidOperationException(status.Detail);
        if (!IsElevated())
        {
            await RunElevatedAsync("install", cancellationToken);
            var elevatedResult = Inspect();
            if (!elevatedResult.Valid) throw new InvalidDataException("The OBS integration did not pass verification after installation.");
            return elevatedResult;
        }
        return await InstallDirectAsync(cancellationToken);
    }

    private async Task<ObsBridgeStatus> InstallDirectAsync(CancellationToken cancellationToken)
    {
        var package = LoadPackage() ?? throw new InvalidDataException("The bundled OBS integration could not be verified.");
        var manifest = package.Manifest;
        var bin = Path.GetDirectoryName(InstalledDll)!;
        var locale = Path.Combine(_installRoot, "data", "locale");
        Directory.CreateDirectory(bin);
        Directory.CreateDirectory(locale);
        await WriteAtomicAsync(package.Dll, InstalledDll, cancellationToken);
        if (package.Locale is not null) await WriteAtomicAsync(package.Locale, Path.Combine(locale, "en-US.ini"), cancellationToken);
        var marker = JsonSerializer.SerializeToUtf8Bytes(manifest, ProtocolV1.JsonOptions);
        var temporary = $"{InstalledManifest}.{Environment.ProcessId}.tmp";
        await File.WriteAllBytesAsync(temporary, marker, cancellationToken);
        File.Move(temporary, InstalledManifest, true);
        var result = Inspect();
        if (!result.Valid) throw new InvalidDataException("The OBS integration did not pass verification after installation.");
        return result;
    }

    public Task RemoveAsync()
    {
        EnsureObsClosed();
        return RemoveCoreAsync();
    }

    private async Task RemoveCoreAsync()
    {
        if (!IsElevated()) await RunElevatedAsync("remove", CancellationToken.None);
        else if (Directory.Exists(_installRoot)) Directory.Delete(_installRoot, true);
    }

    public static bool IsMaintenanceRequest(string[] args) => args.Length is 2 or 3 && args[0].Equals("--manage-obs-bridge", StringComparison.OrdinalIgnoreCase);
    public static bool IsDiagnosticRequest(string[] args) => args.Length == 2 && args[0].Equals("--diagnose-obs-bridge", StringComparison.OrdinalIgnoreCase);

    public static int RunDiagnostic(string[] args)
    {
        if (!IsDiagnosticRequest(args)) return 10;
        try
        {
            var status = new ObsBridgeManager().Inspect();
            File.WriteAllBytes(args[1], JsonSerializer.SerializeToUtf8Bytes(status, ProtocolV1.JsonOptions));
            return status.PackageAvailable ? 0 : 9;
        }
        catch (Exception error)
        {
            try { File.WriteAllText(args[1], JsonSerializer.Serialize(new { packageAvailable = false, detail = error.Message }, ProtocolV1.JsonOptions)); } catch { }
            return 9;
        }
    }

    public static int RunMaintenance(string[] args)
    {
        if (!IsMaintenanceRequest(args) || !IsElevated()) return 6;
        try
        {
            var manager = new ObsBridgeManager();
            EnsureObsClosed();
            if (args[1].Equals("install", StringComparison.OrdinalIgnoreCase)) manager.InstallDirectAsync(CancellationToken.None).GetAwaiter().GetResult();
            else if (args[1].Equals("remove", StringComparison.OrdinalIgnoreCase)) { if (Directory.Exists(manager._installRoot)) Directory.Delete(manager._installRoot, true); }
            else return 7;
            return 0;
        }
        catch (Exception error)
        {
            if (args.Length == 3) try { File.WriteAllText(args[2], error.Message); } catch { }
            return 8;
        }
    }

    private static bool IsElevated()
    {
        if (!OperatingSystem.IsWindows()) return false;
        using var identity = WindowsIdentity.GetCurrent();
        return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
    }

    private static async Task RunElevatedAsync(string action, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(Environment.ProcessPath)) throw new InvalidOperationException("The packaged Companion application is required to manage OBS integration.");
        var resultPath = Path.Combine(Path.GetTempPath(), $"lumi-obs-maintenance-{Guid.NewGuid():N}.txt");
        var start = new ProcessStartInfo(Environment.ProcessPath) { UseShellExecute = true, Verb = "runas", WindowStyle = ProcessWindowStyle.Hidden };
        start.ArgumentList.Add("--manage-obs-bridge");
        start.ArgumentList.Add(action);
        start.ArgumentList.Add(resultPath);
        try
        {
            using var process = Process.Start(start) ?? throw new InvalidOperationException("The OBS integration maintenance process could not start.");
            await process.WaitForExitAsync(cancellationToken);
            if (process.ExitCode != 0)
            {
                string? detail = null;
                try { if (File.Exists(resultPath)) detail = File.ReadAllText(resultPath).Trim(); } catch { }
                throw new InvalidOperationException(string.IsNullOrWhiteSpace(detail) ? "OBS integration maintenance did not complete." : detail);
            }
        }
        catch (Win32Exception error) when (error.NativeErrorCode == 1223)
        {
            throw new InvalidOperationException("Administrator approval was cancelled. Lumi only requests it to manage the OBS plugin under ProgramData.");
        }
        finally { try { File.Delete(resultPath); } catch { } }
    }

    private static void EnsureObsClosed()
    {
        var processes = Process.GetProcessesByName("obs64");
        foreach (var process in processes) process.Dispose();
        if (processes.Length > 0)
            throw new InvalidOperationException("Close OBS before installing, repairing, or removing the managed integration. Companion will never modify a loaded OBS plugin.");
    }
    private static ObsBridgeManifest? ReadManifest(string path) { try { return JsonSerializer.Deserialize<ObsBridgeManifest>(File.ReadAllBytes(path), ProtocolV1.JsonOptions); } catch { return null; } }
    private ObsBridgePackage? LoadPackage()
    {
        var failures = new List<string>();
        var roots = CandidatePackageRoots();
        foreach (var root in roots)
        {
            try
            {
                var manifestBytes = File.ReadAllBytes(Path.Combine(root, "manifest.json"));
                var dll = File.ReadAllBytes(Path.Combine(root, "lumi-obs-bridge.dll"));
                var manifest = ParseManifest(manifestBytes);
                var actualHash = HashBytes(dll);
                if (!actualHash.Equals(manifest.Sha256, StringComparison.OrdinalIgnoreCase))
                    throw new InvalidDataException($"OBS bridge checksum mismatch (expected {manifest.Sha256}, got {actualHash}).");
                _packageFailure = null;
                return new ObsBridgePackage(manifest, dll, File.Exists(Path.Combine(root, "en-US.ini")) ? File.ReadAllBytes(Path.Combine(root, "en-US.ini")) : null);
            }
            catch (Exception error) { failures.Add($"{root}: {error.Message}"); }
        }
        try
        {
            var assembly = Assembly.GetExecutingAssembly();
            var manifestBytes = ReadResource(assembly, "Lumi.Companion.ObsBridge.manifest.json");
            var dll = ReadResource(assembly, "Lumi.Companion.ObsBridge.dll");
            var manifest = ParseManifest(manifestBytes);
            var actualHash = HashBytes(dll);
            if (!actualHash.Equals(manifest.Sha256, StringComparison.OrdinalIgnoreCase))
                throw new InvalidDataException($"Embedded OBS bridge checksum mismatch (expected {manifest.Sha256}, got {actualHash}).");
            _packageFailure = null;
            return new ObsBridgePackage(manifest, dll, TryReadResource(assembly, "Lumi.Companion.ObsBridge.en-US.ini"));
        }
        catch (Exception error)
        {
            failures.Add($"embedded payload: {error.Message}");
            _packageFailure = string.Join(" ", failures);
            return null;
        }
    }
    private static IReadOnlyList<string> CandidatePackageRoots()
    {
        var bases = new List<string?>
        {
            Path.GetDirectoryName(Environment.ProcessPath),
            AppContext.BaseDirectory,
            Path.GetDirectoryName(Environment.GetCommandLineArgs().FirstOrDefault()),
            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Lumi Companion")
        };
        try { bases.Add(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName)); } catch { }
        return bases.Where(value => !string.IsNullOrWhiteSpace(value))
            .Select(value => Path.Combine(value!, "components", "obs-bridge"))
            .Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
    }
    private static ObsBridgeManifest ParseManifest(byte[] value)
    {
        var start = value.Length >= 3 && value[0] == 0xEF && value[1] == 0xBB && value[2] == 0xBF ? 3 : 0;
        using var document = JsonDocument.Parse(value.AsMemory(start));
        var root = document.RootElement;
        var version = root.GetProperty("version").GetString();
        var sha256 = root.GetProperty("sha256").GetString();
        var minimum = root.GetProperty("obs_minimum_version").GetString();
        if (string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(minimum) || sha256 is null || sha256.Length != 64 || !sha256.All(Uri.IsHexDigit))
            throw new InvalidDataException("The OBS bridge manifest is invalid.");
        return new ObsBridgeManifest(version, sha256.ToLowerInvariant(), minimum);
    }
    private static byte[] ReadResource(Assembly assembly, string name) { using var stream = assembly.GetManifestResourceStream(name) ?? throw new FileNotFoundException($"Embedded resource {name} is missing."); using var body = new MemoryStream(); stream.CopyTo(body); return body.ToArray(); }
    private static byte[]? TryReadResource(Assembly assembly, string name) { try { return ReadResource(assembly, name); } catch { return null; } }
    private static string HashFile(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); }
    private static string HashBytes(byte[] value) => Convert.ToHexString(SHA256.HashData(value)).ToLowerInvariant();
    private static async Task WriteAtomicAsync(byte[] value, string target, CancellationToken cancellationToken)
    {
        var temporary = $"{target}.{Environment.ProcessId}.tmp";
        try
        {
            await File.WriteAllBytesAsync(temporary, value, cancellationToken);
            File.Move(temporary, target, true);
        }
        catch
        {
            try { File.Delete(temporary); } catch { }
            throw;
        }
    }
}

public sealed record ObsBridgeManifest(
    [property: JsonPropertyName("version")] string Version,
    [property: JsonPropertyName("sha256")] string Sha256,
    [property: JsonPropertyName("obs_minimum_version")] string ObsMinimumVersion);
public sealed record ObsBridgeStatus(bool PackageAvailable, bool Installed, bool Valid, string? Version, string Detail);
internal sealed record ObsBridgePackage(ObsBridgeManifest Manifest, byte[] Dll, byte[]? Locale);
