using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.LogStack.Flows
{
    // This file is part of the EonaCat project(s) which is released under the Apache License.
    // See the LICENSE file or go to https://EonaCat.com/License for full license details.

    /// <summary>
    /// Audit log severity filter only these levels are written to the audit trail.
    /// </summary>
    public enum AuditLevel
    {
        All,
        WarningAndAbove,
        ErrorAndAbove,
        CriticalOnly,
    }

    /// <summary>
    /// A tamper-evident, append-only audit flow.
    ///
    /// Each entry is written as:
    ///   SEQ|ISO-TIMESTAMP|LEVEL|CATEGORY|MESSAGE|PROPS|HASH
    ///
    /// Where HASH = SHA-256( previousHash + currentLineWithoutHash ).
    /// This creates a hash-chain so any deletion or modification of a past
    /// entry invalidates all subsequent hashes, making tampering detectable.
    ///
    /// The file is opened with FileShare.Read only (no concurrent writers).
    /// The flow is synchronous-by-design: audit entries must land on disk
    /// before the method returns, so <see cref="BlastAsync"/> blocks until
    /// the entry is flushed.
    /// </summary>
    public sealed class AuditFlow : FlowBase
    {
        public event EventHandler<string> OnDirectoryException;
        private const string Delimiter = "|";
        private const int HashLength = 64; // hex SHA-256

        private readonly string _filePath;
        private readonly AuditLevel _auditLevel;
        private readonly bool _includeProperties;

        private readonly object _writeLock = new object();
        private readonly FileStream _stream;
        private readonly StreamWriter _writer;

        private long _sequence;
        private string _previousHash;

        private long _totalEntries;

        public AuditFlow(
            string directory,
            string filePrefix = "audit",
            AuditLevel auditLevel = AuditLevel.All,
            LogLevel minimumLevel = LogLevel.Trace,
            bool includeProperties = true)
            : base("Audit:" + directory, minimumLevel)
        {
            if (directory == null)
            {
                throw new ArgumentNullException(nameof(directory));
            }

            if (filePrefix == null)
            {
                throw new ArgumentNullException(nameof(filePrefix));
            }

            _auditLevel = auditLevel;
            _includeProperties = includeProperties;

            // Resolve relative path
            if (directory.StartsWith("./", StringComparison.Ordinal))
            {
                directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, directory.Substring(2));
            }

            try
            {
                Directory.CreateDirectory(directory);
            }
            catch
            {
                try
                {
                    var processId = Process.GetCurrentProcess().Id;
                    var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString());
                    Directory.CreateDirectory(newDirectory);
                    OnDirectoryException?.Invoke(this, $"AuditFlow: Could not create directory: '{directory}', using directory '{newDirectory}' instead");
                    directory = newDirectory;
                }
                catch
                {
                    var newDirectory = Path.GetTempPath();
                    OnDirectoryException?.Invoke(this, $"AuditFlow: Could not create directory: '{directory}', using directory '{newDirectory}' instead");
                    directory = newDirectory;

                    // Last resort: disable file output by pointing to a safe-ish temp path.
                    // The writer thread still runs and swallows failures.
                }
            }

            // One file per day, named with date stamp
            string date = DateTime.UtcNow.ToString("yyyyMMdd");
            _filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit");

            // Exclusive write access
            _stream = new FileStream(
                _filePath,
                FileMode.Append,
                FileAccess.Write,
                FileShare.Read,   // allow external readers, but no other writers
                bufferSize: 4096,
                FileOptions.WriteThrough);   // WriteThrough = no OS cache, hits disk immediately

            _writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true };

            // Derive starting hash from the last line already in the file (for continuity)
            _previousHash = ReadLastHash(directory, filePrefix, date);
            _sequence = CountExistingLines(_filePath);
        }

        /// <summary>Path to the current audit file.</summary>
        public string FilePath => _filePath;

        /// <summary>Total entries written in this session.</summary>
        public long TotalEntries => Interlocked.Read(ref _totalEntries);

        /// <summary>
        /// Verify the integrity of the audit file by replaying the hash chain.
        /// Returns (true, null) if intact, (false, reason) if tampered.
        /// </summary>
        public static (bool ok, string reason) Verify(string filePath)
        {
            if (!File.Exists(filePath))
            {
                return (false, "File not found.");
            }

            string previousHash = new string('0', HashLength);
            long expectedSeq = 1;

            foreach (string raw in File.ReadLines(filePath, Encoding.UTF8))
            {
                if (string.IsNullOrWhiteSpace(raw) || raw.StartsWith("#"))
                {
                    continue;
                }

                int lastPipe = raw.LastIndexOf(Delimiter, StringComparison.Ordinal);
                if (lastPipe < 0)
                {
                    return (false, $"Malformed line (no delimiter): {Truncate(raw, 120)}");
                }

                string body = raw.Substring(0, lastPipe);
                string storedHash = raw.Substring(lastPipe + 1).Trim();

                if (storedHash.Length != HashLength)
                {
                    return (false, $"Bad hash length on line {expectedSeq}: '{storedHash}'");
                }

                string computedHash = ComputeHash(previousHash, body);

                if (!string.Equals(storedHash, computedHash, StringComparison.OrdinalIgnoreCase))
                {
                    return (false, $"Hash mismatch on sequence {expectedSeq}. " +
                                   $"Expected {computedHash}, found {storedHash}. " +
                                   $"Entry may have been tampered with.");
                }

                // Verify sequence number (first field)
                int firstPipe = body.IndexOf(Delimiter, StringComparison.Ordinal);
                if (firstPipe > 0)
                {
                    string seqStr = body.Substring(0, firstPipe);
                    if (long.TryParse(seqStr, out long seq) && seq != expectedSeq)
                    {
                        return (false, $"Sequence gap: expected {expectedSeq}, found {seq}.");
                    }
                }

                previousHash = computedHash;
                expectedSeq++;
            }

            return (true, null);
        }

        public override Task<WriteResult> BlastAsync(
            LogEvent logEvent,
            CancellationToken cancellationToken = default)
        {
            if (!IsEnabled || !IsLogLevelEnabled(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            if (!PassesAuditLevel(logEvent.Level))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            WriteEntry(logEvent);
            return Task.FromResult(WriteResult.Success);
        }

        public override Task<WriteResult> BlastBatchAsync(
            ReadOnlyMemory<LogEvent> logEvents,
            CancellationToken cancellationToken = default)
        {
            if (!IsEnabled)
            {
                return Task.FromResult(WriteResult.FlowDisabled);
            }

            foreach (var e in logEvents.ToArray())
            {
                if (IsLogLevelEnabled(e) && PassesAuditLevel(e.Level))
                {
                    WriteEntry(e);
                }
            }

            return Task.FromResult(WriteResult.Success);
        }

        public override Task FlushAsync(CancellationToken cancellationToken = default)
        {
            lock (_writeLock)
            {
                _writer.Flush();
                _stream.Flush(flushToDisk: true);
            }
            return Task.CompletedTask;
        }

        public override async ValueTask DisposeAsync()
        {
            IsEnabled = false;
            lock (_writeLock)
            {
                try { _writer.Flush(); } catch { }
                try { _stream.Flush(true); } catch { }
                try { _writer.Dispose(); } catch { }
                try { _stream.Dispose(); } catch { }
            }
            await base.DisposeAsync().ConfigureAwait(false);
        }

        private void WriteEntry(LogEvent log)
        {
            lock (_writeLock)
            {
                long seq = Interlocked.Increment(ref _sequence);

                var sb = new StringBuilder(256);
                sb.Append(seq);
                sb.Append(Delimiter);
                sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O"));
                sb.Append(Delimiter);
                sb.Append(LevelString(log.Level));
                sb.Append(Delimiter);
                sb.Append(Escape(log.Category));
                sb.Append(Delimiter);
                sb.Append(Escape(log.Message.Length > 0 ? log.Message.ToString() : string.Empty));

                if (log.Exception != null)
                {
                    sb.Append(Delimiter);
                    sb.Append("EX=");
                    sb.Append(Escape(log.Exception.GetType().Name + ": " + log.Exception.Message));
                }

                if (_includeProperties && log.Properties.Count > 0)
                {
                    sb.Append(Delimiter);
                    bool first = true;
                    foreach (var kv in log.Properties.ToArray())
                    {
                        if (!first)
                        {
                            sb.Append(';');
                        }

                        first = false;
                        sb.Append(Escape(kv.Key)).Append('=').Append(Escape(kv.Value?.ToString() ?? "null"));
                    }
                }

                string body = sb.ToString();
                string hash = ComputeHash(_previousHash, body);
                string line = body + Delimiter + hash;

                _writer.WriteLine(line);
                // AutoFlush=true + WriteThrough stream = immediate disk write

                _previousHash = hash;
                Interlocked.Increment(ref _totalEntries);
                Interlocked.Increment(ref BlastedCount);
            }
        }

        private bool PassesAuditLevel(LogLevel level) => _auditLevel switch
        {
            AuditLevel.All => true,
            AuditLevel.WarningAndAbove => level >= LogLevel.Warning,
            AuditLevel.ErrorAndAbove => level >= LogLevel.Error,
            AuditLevel.CriticalOnly => level >= LogLevel.Critical,
            _ => true
        };

        private static string LevelString(LogLevel level) => level switch
        {
            LogLevel.Trace => "TRACE",
            LogLevel.Debug => "DEBUG",
            LogLevel.Information => "INFO",
            LogLevel.Warning => "WARN",
            LogLevel.Error => "ERROR",
            LogLevel.Critical => "CRITICAL",
            _ => level.ToString().ToUpperInvariant()
        };

        /// <summary>Replace pipe characters inside field values so the delimiter stays unique.</summary>
        private static string Escape(string value)
            => string.IsNullOrEmpty(value) ? string.Empty : value.Replace("|", "\\|").Replace("\r", "\\r").Replace("\n", "\\n");

        public static string ComputeHash(string previousHash, string body)
        {
            if (string.IsNullOrEmpty(previousHash) || string.IsNullOrEmpty(body))
            {
                throw new ArgumentException("Input values cannot be null or empty.");
            }

            string inputString = previousHash + "|" + body;
            byte[] input = Encoding.UTF8.GetBytes(inputString);

            using (SHA256 sha = SHA256.Create())
            {
                byte[] digest = sha.ComputeHash(input);
                return BitConverter.ToString(digest).Replace("-", "").ToLowerInvariant();
            }
        }

        private static string ReadLastHash(string directory, string prefix, string date)
        {
            string path = Path.Combine(directory, $"{prefix}_{Environment.MachineName}_{date}.audit");

            if (!File.Exists(path))
            {
                return new string('0', HashLength);
            }

            string lastLine = null;

            // Open file with FileShare.ReadWrite to allow reading while it's being written to
            using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (var reader = new StreamReader(fileStream, Encoding.UTF8))
            {
                // Read lines from the file
                while (!reader.EndOfStream)
                {
                    var line = reader.ReadLine();
                    if (!string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
                    {
                        lastLine = line;
                    }
                }
            }

            if (lastLine == null)
            {
                return new string('0', HashLength);
            }

            int lastPipe = lastLine.LastIndexOf(Delimiter, StringComparison.Ordinal);
            return lastPipe >= 0 ? lastLine.Substring(lastPipe + 1).Trim() : new string('0', HashLength);
        }


        private static long CountExistingLines(string path)
        {
            if (!File.Exists(path))
            {
                return 0;
            }

            long count = 0;

            // Open the file with FileShare.ReadWrite to allow concurrent read/write access
            using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (var reader = new StreamReader(fileStream, Encoding.UTF8))
            {
                while (!reader.EndOfStream)
                {
                    var line = reader.ReadLine();
                    if (!string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
                    {
                        count++;
                    }
                }
            }

            return count;
        }


        private static string Truncate(string s, int max)
            => s.Length <= max ? s : s.Substring(0, max) + "...";
    }
}