using System.Collections;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace StellaOps.Feedser.Core.Jobs;

public sealed class JobCoordinator : IJobCoordinator
{
    private readonly JobSchedulerOptions _options;
    private readonly IJobStore _jobStore;
    private readonly ILeaseStore _leaseStore;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<JobCoordinator> _logger;
    private readonly ILoggerFactory _loggerFactory;
    private readonly TimeProvider _timeProvider;
    private readonly JobDiagnostics _diagnostics;
    private readonly string _holderId;

    public JobCoordinator(
        IOptions<JobSchedulerOptions> optionsAccessor,
        IJobStore jobStore,
        ILeaseStore leaseStore,
        IServiceScopeFactory scopeFactory,
        ILogger<JobCoordinator> logger,
        ILoggerFactory loggerFactory,
        TimeProvider timeProvider,
        JobDiagnostics diagnostics)
    {
        _options = (optionsAccessor ?? throw new ArgumentNullException(nameof(optionsAccessor))).Value;
        _jobStore = jobStore ?? throw new ArgumentNullException(nameof(jobStore));
        _leaseStore = leaseStore ?? throw new ArgumentNullException(nameof(leaseStore));
        _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
        _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
        _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
        _holderId = BuildHolderId();
    }

    public async Task<JobTriggerResult> TriggerAsync(string kind, IReadOnlyDictionary<string, object?>? parameters, string trigger, CancellationToken cancellationToken)
    {
        using var triggerActivity = _diagnostics.StartTriggerActivity(kind, trigger);

        if (!_options.Definitions.TryGetValue(kind, out var definition))
        {
            var result = JobTriggerResult.NotFound($"Job kind '{kind}' is not registered.");
            triggerActivity?.SetStatus(ActivityStatusCode.Error, result.ErrorMessage);
            triggerActivity?.SetTag("job.trigger.outcome", result.Outcome.ToString());
            _diagnostics.RecordTriggerRejected(kind, trigger, "not_found");
            return result;
        }

        triggerActivity?.SetTag("job.enabled", definition.Enabled);
        triggerActivity?.SetTag("job.timeout_seconds", definition.Timeout.TotalSeconds);
        triggerActivity?.SetTag("job.lease_seconds", definition.LeaseDuration.TotalSeconds);

        if (!definition.Enabled)
        {
            var result = JobTriggerResult.Disabled($"Job kind '{kind}' is disabled.");
            triggerActivity?.SetStatus(ActivityStatusCode.Ok, "disabled");
            triggerActivity?.SetTag("job.trigger.outcome", result.Outcome.ToString());
            _diagnostics.RecordTriggerRejected(kind, trigger, "disabled");
            return result;
        }

        parameters ??= new Dictionary<string, object?>();

        var parameterSnapshot = parameters.Count == 0
            ? new Dictionary<string, object?>(StringComparer.Ordinal)
            : new Dictionary<string, object?>(parameters, StringComparer.Ordinal);

        if (!TryNormalizeParameters(parameterSnapshot, out var normalizedParameters, out var parameterError))
        {
            var message = string.IsNullOrWhiteSpace(parameterError)
                ? "Job trigger parameters contain unsupported values."
                : parameterError;
            triggerActivity?.SetStatus(ActivityStatusCode.Error, message);
            triggerActivity?.SetTag("job.trigger.outcome", JobTriggerOutcome.InvalidParameters.ToString());
            _diagnostics.RecordTriggerRejected(kind, trigger, "invalid_parameters");
            return JobTriggerResult.InvalidParameters(message);
        }

        parameterSnapshot = normalizedParameters;

        string? parametersHash;
        try
        {
            parametersHash = JobParametersHasher.Compute(parameterSnapshot);
        }
        catch (Exception ex)
        {
            var message = $"Job trigger parameters cannot be serialized: {ex.Message}";
            triggerActivity?.SetStatus(ActivityStatusCode.Error, message);
            triggerActivity?.SetTag("job.trigger.outcome", JobTriggerOutcome.InvalidParameters.ToString());
            _diagnostics.RecordTriggerRejected(kind, trigger, "invalid_parameters");
            _logger.LogWarning(ex, "Failed to serialize parameters for job {Kind}", kind);
            return JobTriggerResult.InvalidParameters(message);
        }

        triggerActivity?.SetTag("job.parameters_count", parameterSnapshot.Count);

        var now = _timeProvider.GetUtcNow();
        var leaseDuration = definition.LeaseDuration <= TimeSpan.Zero ? _options.DefaultLeaseDuration : definition.LeaseDuration;

        JobLease? lease = null;
        try
        {
            lease = await _leaseStore.TryAcquireAsync(definition.LeaseKey, _holderId, leaseDuration, now, cancellationToken).ConfigureAwait(false);
            if (lease is null)
            {
                var result = JobTriggerResult.AlreadyRunning($"Job '{kind}' is already running.");
                triggerActivity?.SetStatus(ActivityStatusCode.Ok, "already_running");
                triggerActivity?.SetTag("job.trigger.outcome", result.Outcome.ToString());
                _diagnostics.RecordTriggerRejected(kind, trigger, "already_running");
                return result;
            }

            var createdAt = _timeProvider.GetUtcNow();
            var request = new JobRunCreateRequest(
                definition.Kind,
                trigger,
                parameterSnapshot,
                parametersHash,
                definition.Timeout,
                leaseDuration,
                createdAt);

            triggerActivity?.SetTag("job.parameters_hash", request.ParametersHash);

            var run = await _jobStore.CreateAsync(request, cancellationToken).ConfigureAwait(false);
            var startedAt = _timeProvider.GetUtcNow();
            var started = await _jobStore.TryStartAsync(run.RunId, startedAt, cancellationToken).ConfigureAwait(false) ?? run;

            triggerActivity?.SetTag("job.run_id", started.RunId);
            triggerActivity?.SetTag("job.created_at", createdAt.UtcDateTime);
            triggerActivity?.SetTag("job.started_at", started.StartedAt?.UtcDateTime ?? startedAt.UtcDateTime);

            var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            if (definition.Timeout > TimeSpan.Zero)
            {
                linkedTokenSource.CancelAfter(definition.Timeout);
            }

            var capturedLease = lease ?? throw new InvalidOperationException("Lease acquisition returned null.");
            try
            {
                _ = Task.Run(() => ExecuteJobAsync(definition, capturedLease, started, parameterSnapshot, trigger, linkedTokenSource), CancellationToken.None)
                    .ContinueWith(t =>
                    {
                        if (t.Exception is not null)
                        {
                            _logger.LogError(t.Exception, "Unhandled job execution failure for {Kind}", definition.Kind);
                        }
                    },
                    TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
                lease = null; // released by background job execution
            }
            catch (Exception ex)
            {
                lease = capturedLease; // ensure outer finally releases if scheduling fails
                triggerActivity?.SetStatus(ActivityStatusCode.Error, ex.Message);
                triggerActivity?.SetTag("job.trigger.outcome", "exception");
                _diagnostics.RecordTriggerRejected(kind, trigger, "queue_failure");
                throw;
            }

            var accepted = JobTriggerResult.Accepted(started);
            _diagnostics.RecordTriggerAccepted(kind, trigger);
            triggerActivity?.SetStatus(ActivityStatusCode.Ok);
            triggerActivity?.SetTag("job.trigger.outcome", accepted.Outcome.ToString());
            return accepted;
        }
        catch (Exception ex)
        {
            triggerActivity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            triggerActivity?.SetTag("job.trigger.outcome", "exception");
            _diagnostics.RecordTriggerRejected(kind, trigger, "exception");
            throw;
        }
        finally
        {
            // Release handled by background execution path. If we failed before scheduling, release here.
            if (lease is not null)
            {
                var releaseError = await TryReleaseLeaseAsync(lease, definition.Kind).ConfigureAwait(false);
                if (releaseError is not null)
                {
                    _logger.LogError(releaseError, "Failed to release lease {LeaseKey} for job {Kind}", lease.Key, definition.Kind);
                }
            }
        }
    }

    public Task<IReadOnlyList<JobDefinition>> GetDefinitionsAsync(CancellationToken cancellationToken)
    {
        IReadOnlyList<JobDefinition> results = _options.Definitions.Values.OrderBy(x => x.Kind, StringComparer.Ordinal).ToArray();
        return Task.FromResult(results);
    }

    public Task<IReadOnlyList<JobRunSnapshot>> GetRecentRunsAsync(string? kind, int limit, CancellationToken cancellationToken)
        => _jobStore.GetRecentRunsAsync(kind, limit, cancellationToken);

    public Task<IReadOnlyList<JobRunSnapshot>> GetActiveRunsAsync(CancellationToken cancellationToken)
        => _jobStore.GetActiveRunsAsync(cancellationToken);

    public Task<JobRunSnapshot?> GetRunAsync(Guid runId, CancellationToken cancellationToken)
        => _jobStore.FindAsync(runId, cancellationToken);

    public Task<JobRunSnapshot?> GetLastRunAsync(string kind, CancellationToken cancellationToken)
        => _jobStore.GetLastRunAsync(kind, cancellationToken);

    public Task<IReadOnlyDictionary<string, JobRunSnapshot>> GetLastRunsAsync(IEnumerable<string> kinds, CancellationToken cancellationToken)
        => _jobStore.GetLastRunsAsync(kinds, cancellationToken);

    private static bool TryNormalizeParameters(
        IReadOnlyDictionary<string, object?> source,
        out Dictionary<string, object?> normalized,
        out string? error)
    {
        if (source.Count == 0)
        {
            normalized = new Dictionary<string, object?>(StringComparer.Ordinal);
            error = null;
            return true;
        }

        normalized = new Dictionary<string, object?>(source.Count, StringComparer.Ordinal);
        foreach (var kvp in source)
        {
            if (string.IsNullOrWhiteSpace(kvp.Key))
            {
                error = "Parameter keys must be non-empty strings.";
                normalized = default!;
                return false;
            }

            try
            {
                normalized[kvp.Key] = NormalizeParameterValue(kvp.Value);
            }
            catch (Exception ex)
            {
                error = $"Parameter '{kvp.Key}' cannot be serialized: {ex.Message}";
                normalized = default!;
                return false;
            }
        }

        error = null;
        return true;
    }

    private static object? NormalizeParameterValue(object? value)
    {
        if (value is null)
        {
            return null;
        }

        switch (value)
        {
            case string or bool or double or decimal:
                return value;
            case byte or sbyte or short or ushort or int or long:
                return Convert.ToInt64(value, CultureInfo.InvariantCulture);
            case uint ui:
                return Convert.ToInt64(ui);
            case ulong ul when ul <= long.MaxValue:
                return (long)ul;
            case ulong ul:
                return ul.ToString(CultureInfo.InvariantCulture);
            case float f:
                return (double)f;
            case DateTime dt:
                return dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime();
            case DateTimeOffset dto:
                return dto.ToUniversalTime();
            case TimeSpan ts:
                return ts.ToString("c", CultureInfo.InvariantCulture);
            case Guid guid:
                return guid.ToString("D");
            case Enum enumValue:
                return enumValue.ToString();
            case byte[] bytes:
                return Convert.ToBase64String(bytes);
            case JsonDocument document:
                return NormalizeJsonElement(document.RootElement);
            case JsonElement element:
                return NormalizeJsonElement(element);
            case IDictionary dictionary:
            {
                var nested = new SortedDictionary<string, object?>(StringComparer.Ordinal);
                foreach (DictionaryEntry entry in dictionary)
                {
                    if (entry.Key is not string key || string.IsNullOrWhiteSpace(key))
                    {
                        throw new InvalidOperationException("Nested dictionary keys must be non-empty strings.");
                    }

                    nested[key] = NormalizeParameterValue(entry.Value);
                }

                return nested;
            }
            case IEnumerable enumerable when value is not string:
            {
                var list = new List<object?>();
                foreach (var item in enumerable)
                {
                    list.Add(NormalizeParameterValue(item));
                }

                return list;
            }
            default:
                throw new InvalidOperationException($"Unsupported parameter value of type '{value.GetType().FullName}'.");
        }
    }

    private static object? NormalizeJsonElement(JsonElement element)
    {
        return element.ValueKind switch
        {
            JsonValueKind.Null => null,
            JsonValueKind.String => element.GetString(),
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            JsonValueKind.Number => element.TryGetInt64(out var l)
                ? l
                : element.TryGetDecimal(out var dec)
                    ? dec
                    : element.GetDouble(),
            JsonValueKind.Object => NormalizeJsonObject(element),
            JsonValueKind.Array => NormalizeJsonArray(element),
            _ => throw new InvalidOperationException($"Unsupported JSON value '{element.ValueKind}'."),
        };
    }

    private static SortedDictionary<string, object?> NormalizeJsonObject(JsonElement element)
    {
        var result = new SortedDictionary<string, object?>(StringComparer.Ordinal);
        foreach (var property in element.EnumerateObject())
        {
            result[property.Name] = NormalizeJsonElement(property.Value);
        }

        return result;
    }

    private static List<object?> NormalizeJsonArray(JsonElement element)
    {
        var items = new List<object?>();
        foreach (var item in element.EnumerateArray())
        {
            items.Add(NormalizeJsonElement(item));
        }

        return items;
    }

    private async Task<JobRunSnapshot?> CompleteRunAsync(Guid runId, JobRunStatus status, string? error, CancellationToken cancellationToken)
    {
        var completedAt = _timeProvider.GetUtcNow();
        var completion = new JobRunCompletion(status, completedAt, error);
        return await _jobStore.TryCompleteAsync(runId, completion, cancellationToken).ConfigureAwait(false);
    }

    private TimeSpan? ResolveDuration(JobRunSnapshot original, JobRunSnapshot? completed)
    {
        if (completed?.Duration is { } duration)
        {
            return duration;
        }

        var startedAt = completed?.StartedAt ?? original.StartedAt ?? original.CreatedAt;
        var completedAt = completed?.CompletedAt ?? _timeProvider.GetUtcNow();
        var elapsed = completedAt - startedAt;
        return elapsed >= TimeSpan.Zero ? elapsed : null;
    }

    private static async Task<Exception?> ObserveLeaseTaskAsync(Task heartbeatTask)
    {
        try
        {
            await heartbeatTask.ConfigureAwait(false);
            return null;
        }
        catch (OperationCanceledException)
        {
            return null;
        }
        catch (Exception ex)
        {
            return ex;
        }
    }

    private async Task<Exception?> TryReleaseLeaseAsync(JobLease lease, string kind)
    {
        try
        {
            await _leaseStore.ReleaseAsync(lease.Key, _holderId, CancellationToken.None).ConfigureAwait(false);
            return null;
        }
        catch (Exception ex)
        {
            return new LeaseMaintenanceException($"Failed to release lease for job '{kind}'.", ex);
        }
    }

    private static Exception? CombineLeaseExceptions(Exception? first, Exception? second)
    {
        if (first is null)
        {
            return second;
        }

        if (second is null)
        {
            return first;
        }

        return new AggregateException(first, second);
    }

    private async Task ExecuteJobAsync(
        JobDefinition definition,
        JobLease lease,
        JobRunSnapshot run,
        IReadOnlyDictionary<string, object?> parameters,
        string trigger,
        CancellationTokenSource linkedTokenSource)
    {
        using (linkedTokenSource)
        {
            var cancellationToken = linkedTokenSource.Token;
            using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            var heartbeatTask = MaintainLeaseAsync(definition, lease, heartbeatCts.Token);

            using var activity = _diagnostics.StartExecutionActivity(run.Kind, trigger, run.RunId);
            activity?.SetTag("job.timeout_seconds", definition.Timeout.TotalSeconds);
            activity?.SetTag("job.lease_seconds", definition.LeaseDuration.TotalSeconds);
            activity?.SetTag("job.parameters_count", parameters.Count);
            activity?.SetTag("job.created_at", run.CreatedAt.UtcDateTime);
            activity?.SetTag("job.started_at", (run.StartedAt ?? run.CreatedAt).UtcDateTime);
            activity?.SetTag("job.parameters_hash", run.ParametersHash);

            _diagnostics.RecordRunStarted(run.Kind);

            JobRunStatus finalStatus = JobRunStatus.Succeeded;
            string? error = null;
            Exception? executionException = null;
            JobRunSnapshot? completedSnapshot = null;
            Exception? leaseException = null;

            try
            {
                using var scope = _scopeFactory.CreateScope();
                var job = (IJob)scope.ServiceProvider.GetRequiredService(definition.JobType);
                var jobLogger = _loggerFactory.CreateLogger(definition.JobType);

                var context = new JobExecutionContext(
                    run.RunId,
                    run.Kind,
                    trigger,
                    parameters,
                    scope.ServiceProvider,
                    _timeProvider,
                    jobLogger);

                await job.ExecuteAsync(context, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException oce)
            {
                finalStatus = JobRunStatus.Cancelled;
                error = oce.Message;
                executionException = oce;
            }
            catch (Exception ex)
            {
                finalStatus = JobRunStatus.Failed;
                error = ex.ToString();
                executionException = ex;
            }
            finally
            {
                heartbeatCts.Cancel();

                leaseException = await ObserveLeaseTaskAsync(heartbeatTask).ConfigureAwait(false);

                var releaseException = await TryReleaseLeaseAsync(lease, definition.Kind).ConfigureAwait(false);
                leaseException = CombineLeaseExceptions(leaseException, releaseException);

                if (leaseException is not null)
                {
                    var leaseMessage = $"Lease maintenance failed: {leaseException.GetType().Name}: {leaseException.Message}";
                    if (finalStatus != JobRunStatus.Failed)
                    {
                        finalStatus = JobRunStatus.Failed;
                        error = leaseMessage;
                        executionException = leaseException;
                    }
                    else
                    {
                        error = string.IsNullOrWhiteSpace(error)
                            ? leaseMessage
                            : $"{error}{Environment.NewLine}{leaseMessage}";
                        executionException = executionException is null
                            ? leaseException
                            : new AggregateException(executionException, leaseException);
                    }
                }
            }

            completedSnapshot = await CompleteRunAsync(run.RunId, finalStatus, error, CancellationToken.None).ConfigureAwait(false);

            if (!string.IsNullOrWhiteSpace(error))
            {
                activity?.SetTag("job.error", error);
            }

            activity?.SetTag("job.status", finalStatus.ToString());

            var completedDuration = ResolveDuration(run, completedSnapshot);
            if (completedDuration.HasValue)
            {
                activity?.SetTag("job.duration_seconds", completedDuration.Value.TotalSeconds);
            }

            switch (finalStatus)
            {
                case JobRunStatus.Succeeded:
                    activity?.SetStatus(ActivityStatusCode.Ok);
                    _logger.LogInformation("Job {Kind} run {RunId} succeeded", run.Kind, run.RunId);
                    break;
                case JobRunStatus.Cancelled:
                    activity?.SetStatus(ActivityStatusCode.Ok, "cancelled");
                    _logger.LogWarning(executionException, "Job {Kind} run {RunId} cancelled", run.Kind, run.RunId);
                    break;
                case JobRunStatus.Failed:
                    activity?.SetStatus(ActivityStatusCode.Error, executionException?.Message ?? error);
                    _logger.LogError(executionException, "Job {Kind} run {RunId} failed", run.Kind, run.RunId);
                    break;
            }

            _diagnostics.RecordRunCompleted(run.Kind, finalStatus, completedDuration, error);
        }
    }

    private async Task MaintainLeaseAsync(JobDefinition definition, JobLease lease, CancellationToken cancellationToken)
    {
        var leaseDuration = lease.LeaseDuration <= TimeSpan.Zero ? _options.DefaultLeaseDuration : lease.LeaseDuration;
        var delay = TimeSpan.FromMilliseconds(Math.Max(1000, leaseDuration.TotalMilliseconds / 2));

        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
            }
            catch (TaskCanceledException)
            {
                break;
            }

            var now = _timeProvider.GetUtcNow();
            try
            {
                await _leaseStore.HeartbeatAsync(definition.LeaseKey, _holderId, leaseDuration, now, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                throw new LeaseMaintenanceException($"Failed to heartbeat lease for job '{definition.Kind}'.", ex);
            }
        }
    }

    private static string BuildHolderId()
    {
        var machine = Environment.MachineName;
        var processId = Environment.ProcessId;
        return $"{machine}:{processId}";
    }
}

internal sealed class LeaseMaintenanceException : Exception
{
    public LeaseMaintenanceException(string message, Exception innerException)
        : base(message, innerException)
    {
    }
}

internal static class JobParametersHasher
{
    internal static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = false,
    };

    public static string? Compute(IReadOnlyDictionary<string, object?> parameters)
    {
        if (parameters is null || parameters.Count == 0)
        {
            return null;
        }

        var canonicalJson = JsonSerializer.Serialize(Sort(parameters), SerializerOptions);
        var bytes = Encoding.UTF8.GetBytes(canonicalJson);
        var hash = SHA256.HashData(bytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private static SortedDictionary<string, object?> Sort(IReadOnlyDictionary<string, object?> parameters)
    {
        var sorted = new SortedDictionary<string, object?>(StringComparer.Ordinal);
        foreach (var kvp in parameters)
        {
            sorted[kvp.Key] = kvp.Value;
        }

        return sorted;
    }
}
