using Microsoft.EntityFrameworkCore;
using Nexus.Api.Data;
using Nexus.Api.Models;
using System.Text.Json;

namespace Nexus.Api.Repositories;

public sealed class OpenClawRunRepository(NexusDbContext db) : IOpenClawRunRepository
{
    public async Task<IReadOnlyList<OpenClawRun>> GetAsync(
        OpenClawRunQuery query,
        CancellationToken cancellationToken = default)
    {
        var runs = db.OpenClawRuns.AsNoTracking().AsQueryable();

        if (!string.IsNullOrWhiteSpace(query.Status))
            runs = runs.Where(run => run.Status == query.Status);
        if (query.TaskId.HasValue)
            runs = runs.Where(run => run.TaskId == query.TaskId);
        if (query.ProjectId.HasValue)
            runs = runs.Where(run => run.ProjectId == query.ProjectId);
        if (!string.IsNullOrWhiteSpace(query.SessionKey))
            runs = runs.Where(run => run.SessionKey == query.SessionKey);
        if (OpenClawRunCursorCodec.TryDecode(query.Cursor, out var cursor))
        {
            var createdBefore = cursor.CreatedAt;
            if (cursor.Id.HasValue)
            {
                var idBefore = cursor.Id.Value;
                runs = runs.Where(run =>
                    run.CreatedAt < createdBefore
                    || (run.CreatedAt == createdBefore
                        && run.Id.CompareTo(idBefore) < 0));
            }
            else
            {
                runs = runs.Where(run => run.CreatedAt < createdBefore);
            }
        }

        return await runs
            .OrderByDescending(run => run.CreatedAt)
            .ThenByDescending(run => run.Id)
            .Take(Math.Clamp(query.Limit, 1, 201))
            .ToListAsync(cancellationToken);
    }

    public Task<OpenClawRun?> GetByIdAsync(
        Guid id,
        bool tracking = false,
        CancellationToken cancellationToken = default)
    {
        var query = tracking
            ? db.OpenClawRuns.AsQueryable()
            : db.OpenClawRuns.AsNoTracking();
        return query.FirstOrDefaultAsync(run => run.Id == id, cancellationToken);
    }

    public Task<OpenClawRun?> GetByStartIdempotencyKeyAsync(
        string idempotencyKey,
        CancellationToken cancellationToken = default)
        => db.OpenClawRuns
            .AsNoTracking()
            .FirstOrDefaultAsync(run => run.StartIdempotencyKey == idempotencyKey, cancellationToken);

    public Task<OpenClawRun?> GetByOpenClawRunIdAsync(
        string openClawRunId,
        CancellationToken cancellationToken = default)
        => db.OpenClawRuns
            .FirstOrDefaultAsync(run => run.OpenClawRunId == openClawRunId, cancellationToken);

    public async Task<IReadOnlyList<OpenClawRunHistory>> GetHistoryAsync(
        Guid runId,
        CancellationToken cancellationToken = default)
        => await db.OpenClawRunHistory
            .AsNoTracking()
            .Where(item => item.RunId == runId)
            .OrderBy(item => item.OccurredAt)
            .ThenBy(item => item.Id)
            .ToListAsync(cancellationToken);

    public Task<OpenClawRunHistory?> GetInvocationAsync(
        Guid runId,
        string action,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
        => db.OpenClawRunHistory
            .AsNoTracking()
            .FirstOrDefaultAsync(
                item => item.RunId == runId
                        && item.Action == action
                        && item.IdempotencyKey == idempotencyKey,
                cancellationToken);

    public Task<bool> HasGatewayEventAsync(
        string gatewayEventId,
        CancellationToken cancellationToken = default)
        => db.OpenClawRunHistory
            .AsNoTracking()
            .AnyAsync(item => item.GatewayEventId == gatewayEventId, cancellationToken);

    public Task<bool> TaskExistsAsync(Guid taskId, CancellationToken cancellationToken = default)
        => db.Tasks.AsNoTracking().AnyAsync(task => task.Id == taskId, cancellationToken);

    public Task<bool> ProjectExistsAsync(Guid projectId, CancellationToken cancellationToken = default)
        => db.Projects.AsNoTracking().AnyAsync(project => project.Id == projectId, cancellationToken);

    public async Task<IReadOnlyList<OpenClawRunSubscription>> GetActiveSubscriptionsAsync(
        CancellationToken cancellationToken = default)
    {
        var subscriptions = await db.OpenClawRuns
            .AsNoTracking()
            .Where(run => run.Status == OpenClawRunStates.Dispatching
                          || run.Status == OpenClawRunStates.Running
                          || run.Status == OpenClawRunStates.Stopping)
            .Select(run => new { run.SessionKey, run.AgentId })
            .Distinct()
            .ToListAsync(cancellationToken);
        return subscriptions
            .Select(item => new OpenClawRunSubscription(item.SessionKey, item.AgentId))
            .ToList();
    }

    public async Task AddAsync(
        OpenClawRun run,
        OpenClawRunHistory history,
        CancellationToken cancellationToken = default)
    {
        db.OpenClawRuns.Add(run);
        db.OpenClawRunHistory.Add(history);
        db.OutboxEvents.Add(CreateRunEvent("run.created", run));
        await db.SaveChangesAsync(cancellationToken);
    }

    public async Task AddRetryAsync(
        OpenClawRun source,
        OpenClawRun retry,
        OpenClawRunHistory sourceHistory,
        OpenClawRunHistory retryHistory,
        CancellationToken cancellationToken = default)
    {
        source.UpdatedAt = DateTimeOffset.UtcNow;
        source.Revision++;
        db.OpenClawRuns.Update(source);
        db.OpenClawRuns.Add(retry);
        db.OpenClawRunHistory.AddRange(sourceHistory, retryHistory);
        db.OutboxEvents.Add(CreateRunEvent("run.updated", source));
        db.OutboxEvents.Add(CreateRunEvent(
            "run.created",
            retry,
            source.Id));
        await db.SaveChangesAsync(cancellationToken);
    }

    public async Task UpdateAsync(
        OpenClawRun run,
        OpenClawRunHistory history,
        CancellationToken cancellationToken = default)
    {
        run.UpdatedAt = DateTimeOffset.UtcNow;
        run.Revision++;
        db.OpenClawRuns.Update(run);
        db.OpenClawRunHistory.Add(history);
        db.OutboxEvents.Add(CreateRunEvent("run.updated", run));
        await db.SaveChangesAsync(cancellationToken);
    }

    public async Task UpdateProjectionAsync(
        OpenClawRun run,
        OpenClawRunHistory? history = null,
        CancellationToken cancellationToken = default)
    {
        run.UpdatedAt = DateTimeOffset.UtcNow;
        run.Revision++;
        db.OpenClawRuns.Update(run);
        if (history is not null)
            db.OpenClawRunHistory.Add(history);
        db.OutboxEvents.Add(CreateRunEvent("run.updated", run));
        await db.SaveChangesAsync(cancellationToken);
    }

    private static OutboxEvent CreateRunEvent(
        string type,
        OpenClawRun run,
        Guid? sourceRunId = null)
        => new()
        {
            Type = type,
            AggregateType = "run",
            AggregateId = run.Id.ToString(),
            AggregateRevision = run.Revision,
            PayloadJson = JsonSerializer.Serialize(new
            {
                status = run.Status,
                taskId = run.TaskId,
                projectId = run.ProjectId,
                agentId = run.AgentId,
                sourceRunId
            })
        };

}
