using Serilog;
using Serilog.Events;
using ZB.MOM.WW.Telemetry.Serilog;

namespace ZB.MOM.WW.ScadaBridge.Host;

/// <summary>
/// Builds the Serilog <see cref="LoggerConfiguration"/> for the Host process.
///
/// The configured minimum level comes from
/// <c>ScadaBridge:Logging:MinimumLevel</c> (bound to <see cref="LoggingOptions"/>) so an
/// operator editing that key changes the effective log level.
///
/// The console and file sinks are read from the standard
/// <c>Serilog</c> configuration section via <c>ReadFrom.Configuration</c> — the sink
/// set, console output template, file path and rolling interval are all
/// configuration-driven (defined in <c>appsettings.json</c>), not hard-coded. The
/// explicit <c>MinimumLevel.Is</c> below pins the floor from <see cref="LoggingOptions"/>.
///
/// <c>ScadaBridge:Logging:MinimumLevel</c> is the single source of truth
/// for the floor — the explicit <c>MinimumLevel.Is</c> call deliberately runs
/// AFTER <c>ReadFrom.Configuration</c> so a <c>Serilog:MinimumLevel</c> entry in
/// configuration is overridden. To make that precedence visible (so an operator
/// who sets <c>Serilog:MinimumLevel</c> does not wonder why the change had no
/// effect), <see cref="Build"/> writes a one-shot warning to
/// <see cref="Console.Error"/> when both keys are present. Pick one path —
/// editing <c>Serilog:MinimumLevel</c> alone has no effect.
/// </summary>
public static class LoggerConfigurationFactory
{
    /// <summary>Builds a <see cref="LoggerConfiguration"/> enriched with node-identity properties and a configured minimum level.</summary>
    /// <param name="configuration">Application configuration supplying the Serilog section and logging options.</param>
    /// <param name="nodeRole">Role label (e.g., <c>central-a</c>) added as a log enrichment property.</param>
    /// <param name="siteId">Site identifier added as a log enrichment property.</param>
    /// <param name="nodeHostname">Hostname added as a log enrichment property.</param>
    /// <returns>The configured <see cref="LoggerConfiguration"/>.</returns>
    public static LoggerConfiguration Build(
        IConfiguration configuration,
        string nodeRole,
        string siteId,
        string nodeHostname)
        => Build(configuration, nodeRole, siteId, nodeHostname, Console.Error);

    /// <summary>
    /// Test-visible overload of <see cref="Build(IConfiguration, string, string, string)"/>
    /// that routes the precedence warning through a caller-supplied
    /// writer so unit tests can capture it. Production calls the four-arg
    /// overload which uses <see cref="Console.Error"/>.
    /// </summary>
    /// <param name="configuration">Application configuration supplying the Serilog section and logging options.</param>
    /// <param name="nodeRole">Role label added as a log enrichment property.</param>
    /// <param name="siteId">Site identifier added as a log enrichment property.</param>
    /// <param name="nodeHostname">Hostname added as a log enrichment property.</param>
    /// <param name="warningWriter">Writer that receives the one-shot override-warning when both keys are present.</param>
    /// <returns>The fully configured <see cref="LoggerConfiguration"/> ready to create the Serilog logger.</returns>
    internal static LoggerConfiguration Build(
        IConfiguration configuration,
        string nodeRole,
        string siteId,
        string nodeHostname,
        TextWriter warningWriter)
    {
        var loggingOptions = new LoggingOptions();
        configuration.GetSection("ScadaBridge:Logging").Bind(loggingOptions);

        var minimumLevel = ParseLevel(loggingOptions.MinimumLevel, warningWriter);

        // Warn once if the operator also set a Serilog:MinimumLevel —
        // they almost certainly expected it to take effect, but the explicit
        // MinimumLevel.Is call below silently overrides it. The warning is
        // emitted only when the conflicting key is actually present (a bare
        // "Default" value is what ReadFrom.Configuration reads); a missing /
        // empty Serilog:MinimumLevel section is silent.
        var serilogMinimumLevel = configuration["Serilog:MinimumLevel"]
            ?? configuration["Serilog:MinimumLevel:Default"];
        if (!string.IsNullOrWhiteSpace(serilogMinimumLevel))
        {
            warningWriter.WriteLine(
                $"warning: Serilog:MinimumLevel ('{serilogMinimumLevel}') is being overridden by " +
                $"ScadaBridge:Logging:MinimumLevel ('{loggingOptions.MinimumLevel ?? "Information (default)"}'). " +
                "ScadaBridge:Logging:MinimumLevel is the documented source of truth for the floor (Host-011); " +
                "remove the Serilog:MinimumLevel entry to silence this warning.");
        }

        return new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            .MinimumLevel.Is(minimumLevel)
            .Enrich.WithProperty("SiteId", siteId)
            .Enrich.WithProperty("NodeHostname", nodeHostname)
            .Enrich.WithProperty("NodeRole", nodeRole)
            .Enrich.With(new TraceContextEnricher());
    }

    /// <summary>
    /// Parses a Serilog <see cref="LogEventLevel"/> name, falling back to
    /// <see cref="LogEventLevel.Information"/> for null/blank/unrecognised values.
    ///
    /// When an operator sets <c>ScadaBridge:Logging:MinimumLevel</c> to a
    /// value that doesn't parse (e.g. the typo "Informaiton"), the helper must NOT
    /// throw — startup has to succeed so the rest of the system can come up — but
    /// it MUST make the silent fallback visible. The logger is not yet built at
    /// this point, so the warning is written directly to <see cref="Console.Error"/>
    /// using <see cref="WriteParseWarning"/>; non-null/non-blank values that fail
    /// to parse are reported once, naming the offending value and the fallback.
    /// Null/blank values are treated as "unset" and silently default — only
    /// explicit-but-invalid values trigger the warning.
    /// </summary>
    /// <param name="level">Configured level string, possibly null/blank/invalid.</param>
    /// <returns>The parsed <see cref="LogEventLevel"/>, or <see cref="LogEventLevel.Information"/> when the value is absent or unrecognised.</returns>
    internal static LogEventLevel ParseLevel(string? level)
        => ParseLevel(level, Console.Error);

    /// <summary>
    /// Test-visible overload of <see cref="ParseLevel(string?)"/> that routes the
    /// one-shot warning through a caller-supplied writer (<see cref="Console.Error"/>
    /// in production) so unit tests can capture the warning output.
    /// </summary>
    /// <param name="level">Configured level string, possibly null/blank/invalid.</param>
    /// <param name="warningWriter">Writer that receives a single warning line if the value is non-blank but unparseable.</param>
    /// <returns>The parsed <see cref="LogEventLevel"/>, or <see cref="LogEventLevel.Information"/> when the value is absent or unrecognised.</returns>
    internal static LogEventLevel ParseLevel(string? level, TextWriter warningWriter)
    {
        if (Enum.TryParse<LogEventLevel>(level, ignoreCase: true, out var parsed))
            return parsed;

        if (!string.IsNullOrWhiteSpace(level))
            warningWriter.WriteLine(
                $"warning: ScadaBridge:Logging:MinimumLevel value '{level}' is not a recognised Serilog LogEventLevel; falling back to Information.");

        return LogEventLevel.Information;
    }
}
