using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Data.Repositories;

public sealed class TaskRepository
{
    private readonly ClaudeDoDbContext _context;

    public TaskRepository(ClaudeDoDbContext context) => _context = context;

    #region CRUD

    public async Task AddAsync(TaskEntity entity, CancellationToken ct = default)
    {
        // Append at bottom of the list by default: SortOrder = max(listId) + 1.
        var maxSort = await _context.Tasks
            .Where(t => t.ListId == entity.ListId)
            .Select(t => (int?)t.SortOrder)
            .MaxAsync(ct);
        entity.SortOrder = (maxSort ?? -1) + 1;

        _context.Tasks.Add(entity);
        await _context.SaveChangesAsync(ct);
    }

    public async Task UpdateAsync(TaskEntity entity, CancellationToken ct = default)
    {
        _context.Tasks.Update(entity);
        await _context.SaveChangesAsync(ct);
    }

    public async Task DeleteAsync(string taskId, CancellationToken ct = default)
    {
        await _context.Tasks.Where(t => t.Id == taskId).ExecuteDeleteAsync(ct);
    }

    public async Task<TaskEntity?> GetByIdAsync(string taskId, CancellationToken ct = default)
    {
        return await _context.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId, ct);
    }

    public async Task<List<TaskEntity>> GetByListIdAsync(string listId, CancellationToken ct = default)
    {
        return await _context.Tasks
            .Where(t => t.ListId == listId)
            .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
            .ToListAsync(ct);
    }

    /// <summary>
    /// Renumbers tasks in a list to 0..N-1 according to <paramref name="orderedTaskIds"/>.
    /// Ids not belonging to the list are ignored; ids missing from the list are untouched.
    /// </summary>
    public async Task ReorderAsync(string listId, IReadOnlyList<string> orderedTaskIds, CancellationToken ct = default)
    {
        if (orderedTaskIds.Count == 0) return;

        var idSet = orderedTaskIds.ToHashSet();
        var tasks = await _context.Tasks
            .Where(t => t.ListId == listId && idSet.Contains(t.Id))
            .ToListAsync(ct);

        for (int i = 0; i < orderedTaskIds.Count; i++)
        {
            var task = tasks.FirstOrDefault(t => t.Id == orderedTaskIds[i]);
            if (task is not null) task.SortOrder = i;
        }

        await _context.SaveChangesAsync(ct);
    }

    // Kept for backwards-compatibility with callers using the old name.
    public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
        => GetByListIdAsync(listId, ct);

    #endregion

    #region Status transitions

    public async Task MarkRunningAsync(string taskId, DateTime startedAt, CancellationToken ct = default)
    {
        await _context.Tasks
            .Where(t => t.Id == taskId)
            .ExecuteUpdateAsync(s => s
                .SetProperty(t => t.Status, TaskStatus.Running)
                .SetProperty(t => t.StartedAt, startedAt), ct);
    }

    public async Task MarkDoneAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct = default)
    {
        await _context.Tasks
            .Where(t => t.Id == taskId)
            .ExecuteUpdateAsync(s => s
                .SetProperty(t => t.Status, TaskStatus.Done)
                .SetProperty(t => t.FinishedAt, finishedAt)
                .SetProperty(t => t.Result, result), ct);
    }

    public async Task MarkFailedAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct = default)
    {
        await _context.Tasks
            .Where(t => t.Id == taskId)
            .ExecuteUpdateAsync(s => s
                .SetProperty(t => t.Status, TaskStatus.Failed)
                .SetProperty(t => t.FinishedAt, finishedAt)
                .SetProperty(t => t.Result, result), ct);
    }

    public async Task SetLogPathAsync(string taskId, string logPath, CancellationToken ct = default)
    {
        await _context.Tasks
            .Where(t => t.Id == taskId)
            .ExecuteUpdateAsync(s => s.SetProperty(t => t.LogPath, logPath), ct);
    }

    public async Task<int> FlipAllRunningToFailedAsync(string reason, CancellationToken ct = default)
    {
        var resultText = "[stale] " + reason;
        var now = DateTime.UtcNow;
        return await _context.Tasks
            .Where(t => t.Status == TaskStatus.Running)
            .ExecuteUpdateAsync(s => s
                .SetProperty(t => t.Status, TaskStatus.Failed)
                .SetProperty(t => t.FinishedAt, now)
                .SetProperty(t => t.Result, resultText), ct);
    }

    public async Task ResetToManualAsync(string taskId, CancellationToken ct = default)
    {
        await _context.Tasks
            .Where(t => t.Id == taskId)
            .ExecuteUpdateAsync(s => s
                .SetProperty(t => t.Status, TaskStatus.Manual)
                .SetProperty(t => t.StartedAt, (DateTime?)null)
                .SetProperty(t => t.FinishedAt, (DateTime?)null)
                .SetProperty(t => t.Result, (string?)null), ct);
    }

    #endregion

    #region Agent settings

    public async Task UpdateAgentSettingsAsync(
        string taskId,
        string? model,
        string? systemPrompt,
        string? agentPath,
        CancellationToken ct = default)
    {
        await _context.Tasks
            .Where(t => t.Id == taskId)
            .ExecuteUpdateAsync(s => s
                .SetProperty(t => t.Model, model)
                .SetProperty(t => t.SystemPrompt, systemPrompt)
                .SetProperty(t => t.AgentPath, agentPath), ct);
    }

    #endregion

    #region Tags

    public async Task AddTagAsync(string taskId, long tagId, CancellationToken ct = default)
    {
        var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct);
        if (task is null) return;
        var tag = await _context.Tags.FindAsync([tagId], ct);
        if (tag is not null && !task.Tags.Any(t => t.Id == tagId))
        {
            task.Tags.Add(tag);
            await _context.SaveChangesAsync(ct);
        }
    }

    public async Task RemoveTagAsync(string taskId, long tagId, CancellationToken ct = default)
    {
        var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct);
        if (task is null) return;
        var tag = task.Tags.FirstOrDefault(t => t.Id == tagId);
        if (tag is not null)
        {
            task.Tags.Remove(tag);
            await _context.SaveChangesAsync(ct);
        }
    }

    public async Task<List<TagEntity>> GetTagsAsync(string taskId, CancellationToken ct = default)
    {
        return await _context.Tasks
            .Where(t => t.Id == taskId)
            .SelectMany(t => t.Tags)
            .ToListAsync(ct);
    }

    public async Task<List<TagEntity>> GetEffectiveTagsAsync(string taskId, CancellationToken ct = default)
    {
        var taskTags = _context.Tasks
            .Where(t => t.Id == taskId)
            .SelectMany(t => t.Tags);
        var listTags = _context.Tasks
            .Where(t => t.Id == taskId)
            .SelectMany(t => t.List.Tags);
        return await taskTags.Union(listTags).Distinct().ToListAsync(ct);
    }

    #endregion

    #region Queue selection

    public async Task<TaskEntity?> GetNextQueuedAgentTaskAsync(DateTime now, CancellationToken ct = default)
    {
        // Atomic queue claim: UPDATE + RETURNING in one statement prevents TOCTOU races.
        // Uses raw SQL because EF cannot express UPDATE...RETURNING.
        // Includes both task-level and list-level "agent" tag so lists tagged "agent"
        // automatically enqueue all their tasks without per-task tagging.
        // EF SQLite stores DateTime as "yyyy-MM-dd HH:mm:ss.fffffff" — use the same format for comparison.
        var nowStr = now.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss.fffffff");
        var result = await _context.Tasks.FromSqlRaw("""
            UPDATE tasks SET status = 'running'
            WHERE id = (
                SELECT t.id FROM tasks t
                WHERE t.status = 'queued'
                  AND (t.scheduled_for IS NULL OR t.scheduled_for <= {0})
                  AND (
                      EXISTS (
                          SELECT 1 FROM task_tags tt
                          JOIN tags tg ON tg.id = tt.tag_id
                          WHERE tt.task_id = t.id AND tg.name = 'agent'
                      )
                      OR EXISTS (
                          SELECT 1 FROM list_tags lt
                          JOIN tags tg ON tg.id = lt.tag_id
                          WHERE lt.list_id = t.list_id AND tg.name = 'agent'
                      )
                  )
                ORDER BY t.sort_order ASC, t.created_at ASC
                LIMIT 1
            )
            RETURNING *
            """, nowStr).ToListAsync(ct);

        return result.FirstOrDefault();
    }

    #endregion
}
