namespace NpncbDiagnostic;

public static class ZipArchiveAnalyzer
{
    private const ushort DeflateMethod = 8;

    public static ZipDiagnosticReport Analyze(
        string zipPath,
        DiagnosticOptions? options = null)
    {
        if (string.IsNullOrWhiteSpace(zipPath))
            throw new ArgumentException("ZIP path is required.", nameof(zipPath));

        options ??= new DiagnosticOptions();
        options.Validate();

        string fullPath = Path.GetFullPath(zipPath);
        if (!File.Exists(fullPath))
            throw new FileNotFoundException("ZIP file was not found.", fullPath);

        ZipContainerMetadata container = ZipContainerReader.ReadMetadata(fullPath, options);
        var diagnostics = new List<ZipEntryDiagnostic>(container.Entries.Count);

        using var stream = new FileStream(
            fullPath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read,
            bufferSize: 64 * 1024,
            FileOptions.RandomAccess);

        foreach (ZipEntryMetadata entry in container.Entries)
        {
            if (entry.IsDirectory)
            {
                diagnostics.Add(CreateSkipped(
                    entry,
                    EntryScanState.SkippedDirectory,
                    "디렉터리 엔트리입니다."));
                continue;
            }

            bool encrypted =
                (entry.Flags & 0x0001) != 0 ||
                (entry.Flags & 0x0040) != 0;

            if (encrypted)
            {
                diagnostics.Add(CreateSkipped(
                    entry,
                    EntryScanState.SkippedEncrypted,
                    "암호화된 엔트리여서 raw DEFLATE 블록을 진단할 수 없습니다."));
                continue;
            }

            if (entry.Method != DeflateMethod)
            {
                diagnostics.Add(CreateSkipped(
                    entry,
                    EntryScanState.SkippedNonDeflate,
                    $"압축 방식 {entry.Method}는 DEFLATE(method 8)가 아닙니다."));
                continue;
            }

            if (entry.CompressedSize > options.MaximumCompressedBytesPerEntry ||
                entry.CompressedSize > int.MaxValue ||
                entry.UncompressedSize > options.MaximumDecodedBytesPerEntry)
            {
                diagnostics.Add(CreateSkipped(
                    entry,
                    EntryScanState.SkippedLimit,
                    "안전 제한을 초과하여 이 엔트리의 DEFLATE 스트림을 건너뛰었습니다."));
                continue;
            }

            try
            {
                byte[] rawDeflate = ZipContainerReader.ReadRawCompressedData(
                    stream,
                    entry,
                    options.MaximumCompressedBytesPerEntry);

                DeflateScanResult deflate = DeflateScanner.Scan(
                    rawDeflate,
                    entry.UncompressedSize,
                    entry.Crc32,
                    options);

                DbaMessageFinding finding = DbaMessageDetector.Analyze(deflate.DbaBits);
                diagnostics.Add(new ZipEntryDiagnostic(
                    entry.Index,
                    entry.Name,
                    entry.Method,
                    entry.Flags,
                    entry.CompressedSize,
                    entry.UncompressedSize,
                    entry.Crc32,
                    EntryScanState.Scanned,
                    null,
                    deflate,
                    finding));
            }
            catch (Exception exception) when (
                exception is InvalidDataException or
                NotSupportedException or
                OverflowException or
                EndOfStreamException)
            {
                diagnostics.Add(new ZipEntryDiagnostic(
                    entry.Index,
                    entry.Name,
                    entry.Method,
                    entry.Flags,
                    entry.CompressedSize,
                    entry.UncompressedSize,
                    entry.Crc32,
                    EntryScanState.Invalid,
                    exception.Message,
                    null,
                    null));
            }
        }

        DiagnosticVerdict verdict = DetermineVerdict(diagnostics);
        string summary = BuildSummary(verdict, diagnostics);

        return new ZipDiagnosticReport(
            fullPath,
            container.FileSize,
            DateTimeOffset.UtcNow,
            verdict,
            summary,
            diagnostics);
    }

    private static ZipEntryDiagnostic CreateSkipped(
        ZipEntryMetadata entry,
        EntryScanState state,
        string note)
    {
        return new ZipEntryDiagnostic(
            entry.Index,
            entry.Name,
            entry.Method,
            entry.Flags,
            entry.CompressedSize,
            entry.UncompressedSize,
            entry.Crc32,
            state,
            note,
            null,
            null);
    }

    private static DiagnosticVerdict DetermineVerdict(
        IReadOnlyList<ZipEntryDiagnostic> entries)
    {
        if (entries.Any(entry =>
                entry.MessageFinding?.Kind == MessageFindingKind.LikelyTextMessage))
        {
            return DiagnosticVerdict.Warning;
        }

        if (entries.Any(entry =>
                entry.MessageFinding?.Kind is
                    MessageFindingKind.PossibleTextMessage or
                    MessageFindingKind.SuspiciousBinary))
        {
            return DiagnosticVerdict.Suspicious;
        }

        if (entries.Any(entry =>
                entry.State is
                    EntryScanState.Invalid or
                    EntryScanState.SkippedEncrypted or
                    EntryScanState.SkippedLimit))
        {
            return DiagnosticVerdict.Inconclusive;
        }

        if (entries.Any(entry => (entry.Deflate?.NpncbCount ?? 0) > 0))
            return DiagnosticVerdict.Informational;

        return DiagnosticVerdict.Clean;
    }

    private static string BuildSummary(
        DiagnosticVerdict verdict,
        IReadOnlyList<ZipEntryDiagnostic> entries)
    {
        int npncbCount = entries.Sum(entry => entry.Deflate?.NpncbCount ?? 0);
        int nonZeroBits = entries.Sum(entry => entry.Deflate?.NonZeroDbaBitCount ?? 0);
        int likelyMessages = entries.Count(entry =>
            entry.MessageFinding?.Kind == MessageFindingKind.LikelyTextMessage);
        int possibleMessages = entries.Count(entry =>
            entry.MessageFinding?.Kind == MessageFindingKind.PossibleTextMessage);

        return verdict switch
        {
            DiagnosticVerdict.Warning =>
                $"경고: NPNCB {npncbCount:N0}개에서 DBA를 연결한 결과, " +
                $"종료자가 있는 UTF-8 메시지 후보 {likelyMessages:N0}개를 발견했습니다.",

            DiagnosticVerdict.Suspicious =>
                $"의심: NPNCB DBA에 0이 아닌 비트 {nonZeroBits:N0}개가 있으며, " +
                $"확정도가 낮은 텍스트 후보 {possibleMessages:N0}개 또는 바이너리 흔적이 있습니다.",

            DiagnosticVerdict.Inconclusive =>
                "판정 보류: 일부 DEFLATE 엔트리를 암호화, 안전 제한 또는 구조 오류 때문에 완전히 검사하지 못했습니다.",

            DiagnosticVerdict.Informational =>
                $"NPNCB {npncbCount:N0}개가 있으나 연결된 DBA 비트는 모두 0입니다. " +
                "정상 flush 또는 인코더가 만든 빈 Stored 블록일 가능성이 높습니다.",

            _ =>
                "분석 가능한 모든 DEFLATE 엔트리에서 NPNCB를 찾지 못했습니다. " +
                "NPNCB DBA 은닉 관점에서는 정상으로 진단합니다."
        };
    }
}
