using System.Text;

namespace NpncbDiagnostic;

/// <summary>
/// Interprets connected NPNCB DBA bits using the companion inserter format:
/// bytes are reconstructed LSB-first and the first 0x00 byte is a terminator.
/// Because this carrier has no mandatory magic value, the result is deliberately
/// reported as a confidence-based finding rather than absolute proof.
/// </summary>
internal static class DbaMessageDetector
{
    private static readonly UTF8Encoding StrictUtf8 = new(false, true);

    public static DbaMessageFinding Analyze(IReadOnlyList<byte> dbaBits)
    {
        if (dbaBits is null) throw new ArgumentNullException(nameof(dbaBits));

        int nonZeroBitCount = 0;
        for (int i = 0; i < dbaBits.Count; i++)
        {
            byte bit = dbaBits[i];
            if (bit > 1)
                throw new ArgumentException("DBA bit values must be 0 or 1.", nameof(dbaBits));
            nonZeroBitCount += bit;
        }

        int wholeByteCount = dbaBits.Count / 8;
        var allWholeBytes = new byte[wholeByteCount];

        for (int byteIndex = 0; byteIndex < wholeByteCount; byteIndex++)
        {
            byte value = 0;
            for (int bitIndex = 0; bitIndex < 8; bitIndex++)
                value |= checked((byte)(dbaBits[byteIndex * 8 + bitIndex] << bitIndex));
            allWholeBytes[byteIndex] = value;
        }

        if (dbaBits.Count == 0)
        {
            return new DbaMessageFinding(
                MessageFindingKind.None,
                allWholeBytes,
                Array.Empty<byte>(),
                null,
                true,
                false,
                wholeByteCount,
                0,
                0,
                1.0,
                "연결할 DBA 비트가 없습니다.");
        }

        if (nonZeroBitCount == 0)
        {
            return new DbaMessageFinding(
                MessageFindingKind.ZeroPaddingOnly,
                allWholeBytes,
                Array.Empty<byte>(),
                string.Empty,
                true,
                false,
                wholeByteCount,
                0,
                0,
                1.0,
                "NPNCB는 존재하지만 모든 DBA 비트가 0입니다. 일반적인 정렬 패딩일 가능성이 높습니다.");
        }

        if (wholeByteCount == 0)
        {
            return new DbaMessageFinding(
                MessageFindingKind.SuspiciousBinary,
                allWholeBytes,
                Array.Empty<byte>(),
                null,
                false,
                false,
                0,
                nonZeroBitCount,
                0,
                0.0,
                "0이 아닌 DBA 비트가 있으나 완전한 1바이트를 만들 만큼 용량이 없습니다.");
        }

        int terminatorIndex = Array.IndexOf(allWholeBytes, (byte)0);
        bool terminatorFound = terminatorIndex >= 0;
        int candidateLength = terminatorFound ? terminatorIndex : allWholeBytes.Length;
        byte[] candidateBytes = allWholeBytes[..candidateLength];

        int consumedBits = terminatorFound
            ? checked((terminatorIndex + 1) * 8)
            : checked(allWholeBytes.Length * 8);
        int nonZeroBitsAfterTerminator = 0;
        for (int bitIndex = consumedBits; bitIndex < dbaBits.Count; bitIndex++)
            nonZeroBitsAfterTerminator += dbaBits[bitIndex];

        string? message = null;
        bool validUtf8 = false;
        double readableRatio = 0.0;
        bool hasVisibleCharacter = false;

        try
        {
            message = StrictUtf8.GetString(candidateBytes);
            validUtf8 = true;
            (readableRatio, hasVisibleCharacter) = MeasureReadability(message);
        }
        catch (DecoderFallbackException)
        {
            // The raw bytes are still retained and reported as a binary finding.
        }

        bool plausibleText =
            candidateBytes.Length > 0 &&
            validUtf8 &&
            hasVisibleCharacter &&
            readableRatio >= 0.90;

        if (plausibleText && terminatorFound && nonZeroBitsAfterTerminator == 0)
        {
            return new DbaMessageFinding(
                MessageFindingKind.LikelyTextMessage,
                allWholeBytes,
                candidateBytes,
                message,
                true,
                true,
                wholeByteCount,
                nonZeroBitCount,
                0,
                readableRatio,
                "유효한 UTF-8 텍스트와 0x00 종료자를 발견했고, 종료자 뒤 DBA는 모두 0입니다.");
        }

        if (plausibleText)
        {
            string explanation = !terminatorFound
                ? "유효한 UTF-8 텍스트가 DBA 전체 용량을 채운 것으로 보이지만 0x00 종료자는 없습니다."
                : "유효한 UTF-8 텍스트와 종료자를 찾았지만 종료자 뒤에도 0이 아닌 DBA 비트가 있습니다.";

            return new DbaMessageFinding(
                MessageFindingKind.PossibleTextMessage,
                allWholeBytes,
                candidateBytes,
                message,
                true,
                terminatorFound,
                wholeByteCount,
                nonZeroBitCount,
                nonZeroBitsAfterTerminator,
                readableRatio,
                explanation);
        }

        string binaryExplanation = validUtf8
            ? "DBA 바이트는 UTF-8로 해석되지만 제어문자 비율 때문에 일반 텍스트로 보기 어렵습니다."
            : "DBA에 0이 아닌 비트가 있으며 복원 바이트는 유효한 UTF-8 텍스트가 아닙니다.";

        return new DbaMessageFinding(
            MessageFindingKind.SuspiciousBinary,
            allWholeBytes,
            candidateBytes,
            message,
            validUtf8,
            terminatorFound,
            wholeByteCount,
            nonZeroBitCount,
            nonZeroBitsAfterTerminator,
            readableRatio,
            binaryExplanation);
    }

    private static (double Ratio, bool HasVisibleCharacter) MeasureReadability(string text)
    {
        if (text.Length == 0) return (1.0, false);

        int readable = 0;
        int total = 0;
        bool hasVisible = false;

        for (int index = 0; index < text.Length; index++)
        {
            char value = text[index];
            total++;

            if (char.IsHighSurrogate(value))
            {
                if (index + 1 < text.Length && char.IsLowSurrogate(text[index + 1]))
                {
                    readable++;
                    hasVisible = true;
                    index++;
                }
                continue;
            }

            if (char.IsLowSurrogate(value)) continue;

            if (char.IsControl(value))
            {
                if (value is '\r' or '\n' or '\t') readable++;
                continue;
            }

            readable++;
            if (!char.IsWhiteSpace(value)) hasVisible = true;
        }

        return (total == 0 ? 1.0 : (double)readable / total, hasVisible);
    }
}
