using System.Text.Json;

namespace Zcbot.WindowsNode;

internal sealed class WorkspaceStore(NodePaths paths)
{
    private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };

    internal string CurrentDirectory(RecoverableJob job) =>
        Path.Combine(paths.WorkspacesDirectory, Binding(job).WorkspaceId.ToString("D"), "current");

    internal void Prepare(RecoverableJob job, string capability)
    {
        var root = WorkspaceRoot(job);
        var current = Path.Combine(root, "current");
        var rollback = Path.Combine(root, "rollback");
        Directory.CreateDirectory(root);
        var binding = Binding(job);
        if (binding.Mode == "continue")
        {
            var metadata = ReadMetadata(root);
            if (metadata.Capability != capability
                || metadata.HeadJobId != binding.SourceJobId)
            {
                throw new InvalidDataException("Workspace head does not match the requested source job.");
            }
            if (!Directory.Exists(current))
            {
                if (Directory.Exists(rollback)) Directory.Move(rollback, current);
                else throw new InvalidDataException("Workspace current state is missing.");
            }
            if (Directory.Exists(rollback)) Directory.Delete(rollback, recursive: true);
            Directory.Move(current, rollback);
        }
        else if (File.Exists(Path.Combine(root, "workspace.json")) || Directory.Exists(current))
        {
            throw new InvalidDataException("A new workspace already contains state.");
        }

        var jobDirectory = Path.Combine(paths.JobsDirectory, job.JobId.ToString("D"));
        AtomicJson(Path.Combine(jobDirectory, "workspace.json"), new
        {
            workspace_id = binding.WorkspaceId,
            source_job_id = binding.SourceJobId,
            mode = binding.Mode,
            local_base_path = binding.Mode == "continue" ? rollback : null,
        });
    }

    internal long Promote(RecoverableJob job, string capability, string stateFilename)
    {
        var root = WorkspaceRoot(job);
        var current = Path.Combine(root, "current");
        var output = Path.Combine(paths.JobsDirectory, job.JobId.ToString("D"), "output");
        var statePath = Path.Combine(output, stateFilename);
        if (!File.Exists(statePath))
        {
            throw new InvalidDataException("Adapter did not create the workspace state output.");
        }
        if (Directory.Exists(current))
        {
            throw new InvalidDataException("Workspace current destination already exists.");
        }
        Directory.Move(output, current);
        return CommitCurrent(job, capability);
    }

    internal long EnsureSucceeded(
        RecoverableJob job, string capability, string stateFilename)
    {
        var current = CurrentDirectory(job);
        if (File.Exists(Path.Combine(current, stateFilename)))
        {
            return CommitCurrent(job, capability);
        }
        var output = Path.Combine(paths.JobsDirectory, job.JobId.ToString("D"), "output");
        if (File.Exists(Path.Combine(output, stateFilename)))
        {
            return Promote(job, capability, stateFilename);
        }
        Restore(job);
        throw new InvalidDataException("Successful workspace output cannot be recovered.");
    }

    private long CommitCurrent(RecoverableJob job, string capability)
    {
        var root = WorkspaceRoot(job);
        var sizeBytes = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
            .Where(item => !Path.GetFileName(item).Equals(
                "workspace.json", StringComparison.OrdinalIgnoreCase))
            .Sum(item => new FileInfo(item).Length);
        AtomicJson(Path.Combine(root, "workspace.json"), new
        {
            workspace_id = Binding(job).WorkspaceId,
            capability,
            head_job_id = job.JobId,
            size_bytes = sizeBytes,
            updated_at = DateTimeOffset.UtcNow,
        });
        AtomicJson(
            Path.Combine(paths.JobsDirectory, job.JobId.ToString("D"), "workspace-result.json"),
            new { size_bytes = sizeBytes });
        return sizeBytes;
    }

    internal void Restore(RecoverableJob job)
    {
        if (Binding(job).Mode != "continue") return;
        var root = WorkspaceRoot(job);
        var current = Path.Combine(root, "current");
        var rollback = Path.Combine(root, "rollback");
        if (!Directory.Exists(current) && Directory.Exists(rollback))
        {
            Directory.Move(rollback, current);
        }
    }

    private string WorkspaceRoot(RecoverableJob job) =>
        Path.Combine(paths.WorkspacesDirectory, Binding(job).WorkspaceId.ToString("D"));

    private static WorkspaceBinding Binding(RecoverableJob job) =>
        job.Workspace
        ?? throw new InvalidDataException("Job has no persistent workspace binding.");

    private static WorkspaceMetadata ReadMetadata(string root)
    {
        var path = Path.Combine(root, "workspace.json");
        using var document = JsonDocument.Parse(File.ReadAllBytes(path));
        var value = document.RootElement;
        if (!value.TryGetProperty("capability", out var capability)
            || !value.TryGetProperty("head_job_id", out var head)
            || !Guid.TryParse(head.GetString(), out var headJobId))
        {
            throw new InvalidDataException("Workspace metadata is invalid.");
        }
        return new WorkspaceMetadata(capability.GetString() ?? "", headJobId);
    }

    private static void AtomicJson(string path, object value)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        var temporary = path + ".tmp-" + Guid.NewGuid().ToString("N");
        try
        {
            using (var stream = new FileStream(
                temporary,
                FileMode.CreateNew,
                FileAccess.Write,
                FileShare.None,
                4096,
                FileOptions.WriteThrough))
            {
                var content = JsonSerializer.SerializeToUtf8Bytes(value, JsonOptions);
                stream.Write(content);
                stream.Flush(flushToDisk: true);
            }
            File.Move(temporary, path, overwrite: true);
        }
        finally
        {
            if (File.Exists(temporary)) File.Delete(temporary);
        }
    }

    private sealed record WorkspaceMetadata(string Capability, Guid HeadJobId);
}
