namespace BigRegister.Api.Data;

/// <summary>
/// Stored document: metadata + bytes. The demo persists bytes in the SQLite file
/// (WP-22) purely so a re-opened wizard can preview/download what was uploaded — a
/// real backend persists them to blob storage keyed by DocumentId. Bytes are never
/// serialized into a JSON response; only the dedicated content endpoint streams them.
/// </summary>
public sealed record StoredDocument(
    string DocumentId, string LocalId, string CategoryId, string WizardId,
    string FileName, long SizeBytes, string ContentType, byte[] Content, string Owner, DateTimeOffset UploadedAt)
{
  public bool Linked { get; set; }
}

/// <summary>Id is EF Core's auto-increment key — not part of the positional
/// constructor, so every existing `new AuditEntry(at, action, ...)` call site
/// keeps working unchanged; EF Core assigns it on insert.</summary>
public sealed record AuditEntry(DateTimeOffset At, string Action, string DocumentId, string CategoryId, string Actor)
{
  public long Id { get; init; }
}

/// <summary>
/// EF Core/SQLite-backed document store + audit log (WP-22 — was a static
/// Dictionary). ponytail: one global lock, same as before — SQLite tolerates only
/// one writer at a time anyway, and this process already serialized all access
/// through a single gate, so it now doubles as a coarse single-writer guard for
/// the DB file. The audit log holds metadata only (never file content or other PII).
/// </summary>
public static class DocumentStore
{
  /// The single seeded user (the demo has no real auth; ownership = this id).
  public const string DemoOwner = "19012345601";

  private static readonly object _gate = new();

  public static StoredDocument Add(string localId, string categoryId, string wizardId, string fileName, string contentType, byte[] content, string owner)
  {
    var doc = new StoredDocument(Guid.NewGuid().ToString(), localId, categoryId, wizardId, fileName, content.LongLength, contentType, content, owner, DateTimeOffset.UtcNow);
    lock (_gate)
    {
      using var db = Db.Create();
      db.Documents.Add(doc);
      db.SaveChanges();
    }
    Audit("upload", doc.DocumentId, categoryId, owner);
    return doc;
  }

  public static StoredDocument? Get(string documentId)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      return db.Documents.Find(documentId);
    }
  }

  /// Status for the poll-on-return pattern: a known localId is "complete" (it
  /// arrived), an unknown one is still in flight / never started.
  public static IReadOnlyList<StoredDocument> ByLocalIds(IEnumerable<string> localIds)
  {
    var set = localIds.ToHashSet();
    lock (_gate)
    {
      using var db = Db.Create();
      return db.Documents.Where(d => set.Contains(d.LocalId)).ToList();
    }
  }

  /// Mark digital documents as linked to a finalised submission (blocks user delete).
  public static void Link(IEnumerable<string> documentIds)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      foreach (var id in documentIds)
      {
        var d = db.Documents.Find(id);
        if (d is not null) d.Linked = true;
      }
      db.SaveChanges();
    }
  }

  public enum DeleteResult { Ok, NotFound, Linked }

  /// User delete: owner-scoped; blocked once linked to a finalised submission.
  public static DeleteResult DeleteOwned(string documentId, string owner)
  {
    string categoryId;
    lock (_gate)
    {
      using var db = Db.Create();
      var d = db.Documents.Find(documentId);
      if (d is null || d.Owner != owner) return DeleteResult.NotFound;
      if (d.Linked) return DeleteResult.Linked;
      categoryId = d.CategoryId;
      db.Documents.Remove(d);
      db.SaveChanges();
    }
    Audit("delete-user", documentId, categoryId, owner);
    return DeleteResult.Ok;
  }

  /// Admin delete: bypasses ownership, unlinks, and (seam) flags the submission for
  /// review so a caseworker is notified. Returns false if the document is unknown.
  public static bool AdminDelete(string documentId, string actor)
  {
    string categoryId;
    lock (_gate)
    {
      using var db = Db.Create();
      var d = db.Documents.Find(documentId);
      if (d is null) return false;
      categoryId = d.CategoryId;
      db.Documents.Remove(d);
      db.SaveChanges();
    }
    Audit("delete-admin", documentId, categoryId, actor);
    return true;
  }

  public static void Audit(string action, string documentId, string categoryId, string actor)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      db.AuditEntries.Add(new AuditEntry(DateTimeOffset.UtcNow, action, documentId, categoryId, actor));
      db.SaveChanges();
    }
  }

  public static IReadOnlyList<AuditEntry> AuditLog
  {
    get
    {
      lock (_gate)
      {
        using var db = Db.Create();
        return db.AuditEntries.ToList();
      }
    }
  }
}
