namespace NpncbDiagnostic;

public enum DiagnosticVerdict
{
    Clean = 0,
    Informational = 1,
    Inconclusive = 2,
    Suspicious = 3,
    Warning = 4
}

public enum EntryScanState
{
    Scanned,
    SkippedDirectory,
    SkippedNonDeflate,
    SkippedEncrypted,
    SkippedLimit,
    Invalid
}

public enum MessageFindingKind
{
    None,
    ZeroPaddingOnly,
    LikelyTextMessage,
    PossibleTextMessage,
    SuspiciousBinary
}

public sealed record DiagnosticOptions(
    int MaximumEntries = 100_000,
    long MaximumCompressedBytesPerEntry = 512L * 1024L * 1024L,
    long MaximumDecodedBytesPerEntry = 1024L * 1024L * 1024L,
    int MaximumPhysicalBlocksPerEntry = 2_000_000,
    int MaximumDbaBitsPerEntry = 64 * 1024 * 1024)
{
    public void Validate()
    {
        if (MaximumEntries <= 0) throw new ArgumentOutOfRangeException(nameof(MaximumEntries));
        if (MaximumCompressedBytesPerEntry <= 0)
            throw new ArgumentOutOfRangeException(nameof(MaximumCompressedBytesPerEntry));
        if (MaximumDecodedBytesPerEntry <= 0)
            throw new ArgumentOutOfRangeException(nameof(MaximumDecodedBytesPerEntry));
        if (MaximumPhysicalBlocksPerEntry <= 0)
            throw new ArgumentOutOfRangeException(nameof(MaximumPhysicalBlocksPerEntry));
        if (MaximumDbaBitsPerEntry <= 0)
            throw new ArgumentOutOfRangeException(nameof(MaximumDbaBitsPerEntry));
    }
}

public sealed record NpncbBlockFinding(
    int PhysicalBlockIndex,
    bool IsFinal,
    long BlockStartBit,
    long DbaStartBit,
    int DbaBitCount,
    byte DbaValue,
    int NonZeroBitCount);

public sealed record DeflateScanResult(
    int PhysicalBlockCount,
    int StoredDataBlockCount,
    int FixedBlockCount,
    int DynamicBlockCount,
    int NpncbCount,
    int DbaBitCount,
    int NonZeroDbaBitCount,
    IReadOnlyList<byte> DbaBits,
    IReadOnlyList<NpncbBlockFinding> NpncbBlocks,
    long DecodedByteCount,
    uint ComputedCrc32,
    int FinalTrailingBitCount,
    int NonZeroFinalTrailingBitCount);

public sealed record DbaMessageFinding(
    MessageFindingKind Kind,
    byte[] ReconstructedWholeBytes,
    byte[] CandidateBytes,
    string? Message,
    bool IsValidUtf8,
    bool TerminatorFound,
    int WholeDbaByteCount,
    int NonZeroDbaBitCount,
    int NonZeroBitsAfterTerminator,
    double ReadableCharacterRatio,
    string Explanation)
{
    public bool HasTextMessage =>
        Kind is MessageFindingKind.LikelyTextMessage or MessageFindingKind.PossibleTextMessage;
}

public sealed record ZipEntryDiagnostic(
    int Index,
    string Name,
    ushort CompressionMethod,
    ushort GeneralPurposeFlags,
    long CompressedSize,
    long UncompressedSize,
    uint ExpectedCrc32,
    EntryScanState State,
    string? Note,
    DeflateScanResult? Deflate,
    DbaMessageFinding? MessageFinding)
{
    public bool IsEncrypted =>
        (GeneralPurposeFlags & 0x0001) != 0 ||
        (GeneralPurposeFlags & 0x0040) != 0;
}

public sealed record ZipDiagnosticReport(
    string ZipPath,
    long ZipFileSize,
    DateTimeOffset ScannedAtUtc,
    DiagnosticVerdict Verdict,
    string Summary,
    IReadOnlyList<ZipEntryDiagnostic> Entries)
{
    public int ScannedDeflateEntryCount => Entries.Count(entry => entry.State == EntryScanState.Scanned);
    public int InvalidEntryCount => Entries.Count(entry => entry.State == EntryScanState.Invalid);
    public int InaccessibleDeflateEntryCount => Entries.Count(entry =>
        entry.State is EntryScanState.SkippedEncrypted or EntryScanState.SkippedLimit);

    public int TotalPhysicalBlockCount => Entries.Sum(entry => entry.Deflate?.PhysicalBlockCount ?? 0);
    public int TotalNpncbCount => Entries.Sum(entry => entry.Deflate?.NpncbCount ?? 0);
    public int TotalDbaBitCount => Entries.Sum(entry => entry.Deflate?.DbaBitCount ?? 0);
    public int TotalNonZeroDbaBitCount => Entries.Sum(entry => entry.Deflate?.NonZeroDbaBitCount ?? 0);

    public int LikelyMessageCount => Entries.Count(entry =>
        entry.MessageFinding?.Kind == MessageFindingKind.LikelyTextMessage);

    public int PossibleMessageCount => Entries.Count(entry =>
        entry.MessageFinding?.Kind == MessageFindingKind.PossibleTextMessage);

    public int SuspiciousBinaryCount => Entries.Count(entry =>
        entry.MessageFinding?.Kind == MessageFindingKind.SuspiciousBinary);
}
