using BigRegister.Api.Contracts;
using BigRegister.Domain.Authorization;
using Microsoft.EntityFrameworkCore;

namespace BigRegister.Api.Data;

/// <summary>
/// The letter (brief) — one demo brief per owner, created from a template on first
/// read. EF Core/SQLite-backed (WP-22 — was a static Dictionary), mirrors
/// <see cref="ApplicationStore"/>. The status machine and its guards live here (the
/// server is authoritative for transitions); the FE mirrors them in its pure reducer
/// for UX. Rich-text content is stored opaquely as DTOs — the stub does not
/// interpret it (no server-side placeholder linting in this slice).
/// </summary>
public sealed class BriefEntity
{
  public required string BriefId { get; init; }
  public required string Owner { get; init; }
  public required string Beroep { get; init; }
  public required string TemplateId { get; init; }
  public required string DrafterId { get; init; }
  public required IReadOnlyList<PlaceholderDefDto> Placeholders { get; init; }
  public List<LetterSectionDto> Sections { get; set; } = new();
  public BriefStatusDto Status { get; set; } = new("draft");
  /// Which sub-organization's org template themes this letter (WP-23).
  public string SubOrgId { get; set; } = OrgTemplateSeed.Registers;
  /// Pinned at send: sent letters are immutable, a republish never re-themes them.
  public int? SentOrgTemplateVersion { get; set; }

  public BriefDto ToDto() => new(BriefId, Beroep, TemplateId, Placeholders, Sections, Status, DrafterId);
}

/// <summary>ponytail: one global lock, same as before this WP — SQLite tolerates
/// only one writer at a time anyway, and this was already a single coarse gate.</summary>
public static class BriefStore
{
  // Dev-only role stand-ins (no real identities in this POC — see the ?role= toggle).
  public const string DrafterId = "demo-drafter";
  public const string ApproverId = "demo-approver";
  public const string AdminId = "demo-admin";

  public enum Outcome { Ok, Forbidden, Conflict }

  private static readonly object _gate = new();

  public static BriefEntity GetOrCreate(string owner)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      var existing = db.Briefs.FirstOrDefault(e => e.Owner == owner);
      if (existing is not null) return existing;
      var created = BriefSeed.NewBrief(owner);
      db.Briefs.Add(created);
      db.SaveChanges();
      return created;
    }
  }

  /// Save draft content. Drafter-only; only editable in draft/rejected; a rejected
  /// letter reopens to draft on edit (mirrors the FE reducer).
  public static (Outcome, BriefEntity?) Save(string owner, IReadOnlyList<LetterSectionDto> sections, bool isDrafter)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      var e = db.Briefs.FirstOrDefault(e => e.Owner == owner);
      if (e is null) return (Outcome.Conflict, null);
      if (!isDrafter) return (Outcome.Forbidden, null);
      if (e.Status.Tag is not ("draft" or "rejected")) return (Outcome.Conflict, null);
      e.Sections = sections.ToList();
      if (e.Status.Tag == "rejected") e.Status = new BriefStatusDto("draft");
      db.SaveChanges();
      return (Outcome.Ok, e);
    }
  }

  public static (Outcome, BriefEntity?) Submit(string owner, bool isDrafter, string at)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      var e = db.Briefs.FirstOrDefault(e => e.Owner == owner);
      if (e is null) return (Outcome.Conflict, null);
      if (!isDrafter) return (Outcome.Forbidden, null);
      if (e.Status.Tag != "draft" || !RequiredFilled(e)) return (Outcome.Conflict, null);
      e.Status = new BriefStatusDto("submitted", SubmittedBy: e.DrafterId, SubmittedAt: at);
      db.SaveChanges();
      return (Outcome.Ok, e);
    }
  }

  public static (Outcome, BriefEntity?) Approve(string owner, Principal principal, string at) =>
      Review(owner, principal, BriefAction.Approve, () =>
          new BriefStatusDto("approved", ApprovedBy: Authz.ActingId(principal), ApprovedAt: at));

  public static (Outcome, BriefEntity?) Reject(string owner, Principal principal, string comments, string at) =>
      Review(owner, principal, BriefAction.Reject, () =>
          new BriefStatusDto("rejected", RejectedBy: Authz.ActingId(principal), RejectedAt: at, Comments: comments));

  public static (Outcome, BriefEntity?) Send(string owner, string at)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      var e = db.Briefs.FirstOrDefault(e => e.Owner == owner);
      if (e is null) return (Outcome.Conflict, null);
      if (e.Status.Tag != "approved") return (Outcome.Conflict, null);
      e.Status = new BriefStatusDto("sent", SentAt: at);
      // Pin the org-template version the letter was sent with (WP-23): from here on
      // its appearance is frozen — republishing the template touches unsent briefs only.
      e.SentOrgTemplateVersion = OrgTemplateStore.PublishedVersionOf(e.SubOrgId);
      db.SaveChanges();
      return (Outcome.Ok, e);
    }
  }

  /// Test seam: clear the demo brief between tests.
  public static void Reset()
  {
    lock (_gate)
    {
      using var db = Db.Create();
      db.Briefs.ExecuteDelete();
    }
  }

  /// Demo affordance: drop the owner's brief and create a fresh one. No guards — a
  /// "start over" for the showcase, not a real workflow transition.
  public static BriefEntity ResetAndCreate(string owner)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      db.Briefs.Where(e => e.Owner == owner).ExecuteDelete();
      var created = BriefSeed.NewBrief(owner);
      db.Briefs.Add(created);
      db.SaveChanges();
      return created;
    }
  }

  // Approve/reject share the guard: must be submitted, and the approver must differ
  // from the drafter (a drafter cannot approve their own letter). The SoD check is
  // Authz.CanActOn — the SAME check the screen DTO's decision flags use — checked
  // BEFORE the status guard so Forbidden vs Conflict ordering matches the old
  // inline check exactly.
  private static (Outcome, BriefEntity?) Review(string owner, Principal principal, BriefAction action, Func<BriefStatusDto> next)
  {
    lock (_gate)
    {
      using var db = Db.Create();
      var e = db.Briefs.FirstOrDefault(e => e.Owner == owner);
      if (e is null) return (Outcome.Conflict, null);
      if (!Authz.CanActOn(action, principal, e.DrafterId)) return (Outcome.Forbidden, null);
      if (e.Status.Tag != "submitted") return (Outcome.Conflict, null);
      e.Status = next();
      db.SaveChanges();
      return (Outcome.Ok, e);
    }
  }

  private static bool RequiredFilled(BriefEntity e) => e.Sections.All(s => !s.Required || s.Blocks.Count > 0);
}

/// <summary>Seeded template (sections + placeholder fields) and passage library.</summary>
public static class BriefSeed
{
  public const string TemplateId = "besluit-arts";
  public const string Beroep = "arts";

  private static RichTextNodeDto T(string t) => new("text", Text: t);
  private static RichTextNodeDto P(string key) => new("placeholder", Key: key);
  private static RichTextBlockDto Block(params RichTextNodeDto[] nodes) => new(new[] { new ParagraphDto(nodes) });

  // The template's valid placeholder fields. Two are flagged to exercise the linter:
  // a deprecated field and one not fillable for this beroep.
  private static readonly IReadOnlyList<PlaceholderDefDto> Placeholders = new[]
  {
        new PlaceholderDefDto("naam_zorgverlener", "Naam zorgverlener", true),
        new PlaceholderDefDto("datum", "Datum", true),
        new PlaceholderDefDto("big_nummer", "BIG-nummer", true),
        new PlaceholderDefDto("reden_besluit", "Reden besluit", false),
        new PlaceholderDefDto("oud_kenmerk", "Oud kenmerk", true, Deprecated: true),
        new PlaceholderDefDto("specialisme_code", "Specialismecode", true, Fillable: false),
    };

  public static BriefEntity NewBrief(string owner) => new()
  {
    BriefId = Guid.NewGuid().ToString(),
    Owner = owner,
    Beroep = Beroep,
    TemplateId = TemplateId,
    DrafterId = BriefStore.DrafterId,
    Placeholders = Placeholders,
    // aanhef + slot are predefined, locked template text (prefilled); the drafter
    // composes only the unlocked `kern`. Save trusts the FE not to mutate locked
    // sections (FE-authoritative for this POC — the FE reducer also refuses it).
    Sections = new()
        {
            new("aanhef", "Aanhef", true, new List<LetterBlockDto>
            {
                new("freeText", "aanhef-1", Block(T("Geachte heer/mevrouw "), P("naam_zorgverlener"), T(","))),
            }, Locked: true),
            new("kern", "Kern van het besluit", true, new List<LetterBlockDto>()),
            new("slot", "Slot", false, new List<LetterBlockDto>
            {
                new("freeText", "slot-1", Block(T("Met vriendelijke groet,"))),
            }, Locked: true),
        },
    Status = new BriefStatusDto("draft"),
  };

  /// Global passages plus those scoped to the given beroep.
  public static IReadOnlyList<LibraryPassageDto> PassagesFor(string beroep)
  {
    var all = new List<LibraryPassageDto>
        {
            new("p-aanhef-1", "global", "aanhef", "Standaard aanhef",
                Block(T("Geachte heer/mevrouw "), P("naam_zorgverlener"), T(",")), 1),
            new("p-kern-1", "global", "kern", "Beoordeling ontvangen",
                Block(T("Op "), P("datum"), T(" hebben wij uw aanvraag beoordeeld.")), 1),
            new("p-kern-arts", "beroep", "kern", "Toelichting arts",
                Block(T("Als arts met BIG-nummer "), P("big_nummer"), T(" delen wij u het volgende mee.")), 1, Beroep: "arts"),
            new("p-slot-1", "global", "slot", "Standaard slot",
                Block(T("Met vriendelijke groet,")), 1),
            // These reference a deprecated / not-fillable field so the linter's
            // warning + error states are visible in the demo once inserted.
            new("p-kern-oud", "global", "kern", "Verwijzing oud kenmerk (verouderd)",
                Block(T("Zie kenmerk "), P("oud_kenmerk"), T(".")), 1),
            new("p-slot-code", "global", "slot", "Specialismecode (niet invulbaar)",
                Block(T("Specialisme: "), P("specialisme_code"), T(".")), 1),
        };
    return all.Where(p => p.Scope == "global" || p.Beroep == beroep).ToList();
  }
}
