using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Services;

namespace ClaudeDo.Ui.ViewModels.Conflicts;

public sealed partial class ConflictResolverViewModel : ObservableObject
{
    private readonly IWorkerClient _worker;
    private readonly string _taskId;

    // The task whose conflicted working tree is read/written. For a single-task merge this is
    // _taskId; for a planning unit-merge it's the subtask currently being merged.
    private string _conflictTaskId;

    // When set, this is a planning unit-merge: continue/abort drive the orchestrator on the parent.
    private string? _planningParentId;

    public ObservableCollection<MergeFile> Files { get; } = new();

    // All text conflicts across all files, flattened for one-at-a-time navigation.
    private readonly List<(MergeFile File, MergeConflictBlock Block)> _flat = new();

    [ObservableProperty] private bool _isBusy;
    [ObservableProperty] private string? _error;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(ContinueHint))]
    private bool _canContinue;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasCurrent))]
    private MergeConflictBlock? _current;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(PositionText))]
    [NotifyPropertyChangedFor(nameof(CurrentPath))]
    [NotifyCanExecuteChangedFor(nameof(NextCommand))]
    [NotifyCanExecuteChangedFor(nameof(PreviousCommand))]
    private int _currentIndex = -1;

    [ObservableProperty] private MergeFile? _activeFile;

    /// <summary>Raised when the active file changes so the view can rebuild its three documents.</summary>
    public event Action? ActiveFileChanged;

    partial void OnActiveFileChanged(MergeFile? value)
    {
        ActiveFileChanged?.Invoke();
        OnPropertyChanged(nameof(ActiveOursText));
        OnPropertyChanged(nameof(ActiveTheirsText));
        OnPropertyChanged(nameof(ActiveResultText));
        OnPropertyChanged(nameof(PositionText));
        // Keep the focused conflict inside the active file (e.g. when switched via the file picker).
        if (value is not null && (Current is null || !value.Conflicts.Contains(Current)))
        {
            var idx = _flat.FindIndex(x => x.File == value);
            if (idx >= 0) MoveTo(idx);
        }
    }

    public string ActiveOursText   => ActiveFile?.OursText   ?? "";
    public string ActiveTheirsText => ActiveFile?.TheirsText ?? "";
    public string ActiveResultText => ActiveFile?.ResultText ?? "";

    public bool HasCurrent => Current is not null;
    public int TotalConflicts => _flat.Count;
    public int ResolvedCount => _flat.Count(x => x.Block.IsResolved);
    public string? CurrentPath => InRange ? _flat[CurrentIndex].File.Path : null;

    public string PositionText
    {
        get
        {
            if (ActiveFile is null || ActiveFile.Conflicts.Count == 0) return "No text conflicts";
            var count = ActiveFile.Conflicts.Count;
            var resolved = ActiveFile.Conflicts.Count(c => c.IsResolved);
            return $"{count} {(count == 1 ? "conflict" : "conflicts")} · {resolved} resolved";
        }
    }

    public IReadOnlyList<string> BinaryFilePaths => Files.Where(f => f.IsBinary).Select(f => f.Path).ToList();
    public bool HasBinaryFiles => Files.Any(f => f.IsBinary);

    public bool HasMultipleFiles => Files.Count > 1;

    /// <summary>Cross-file progress shown in the editor: how many files still have unresolved
    /// (or binary) conflicts, so you can see how many more need attention.</summary>
    public string FilesSummary
    {
        get
        {
            var total = Files.Count;
            if (total == 0) return "";
            var unresolved = Files.Count(f => !f.AllResolved);
            return unresolved == 0 ? $"All {total} files resolved" : $"{unresolved} of {total} files unresolved";
        }
    }

    public string ContinueHint
    {
        get
        {
            if (HasBinaryFiles)
                return "Binary conflicts must be resolved externally — abort and resolve in your editor.";
            var openFiles = Files.Count(f => !f.AllResolved);
            if (openFiles == 0) return "";
            var openConflicts = _flat.Count(x => !x.Block.IsResolved);
            return $"{openConflicts} {(openConflicts == 1 ? "conflict" : "conflicts")} in " +
                   $"{openFiles} {(openFiles == 1 ? "file" : "files")} still unresolved.";
        }
    }

    private bool InRange => CurrentIndex >= 0 && CurrentIndex < _flat.Count;

    public string TaskId => _taskId;
    public Action? CloseRequested { get; set; }

    /// <summary>Raised when the current conflict changes so the view can reload its editors.</summary>
    public event Action? CurrentChanged;

    public ConflictResolverViewModel(IWorkerClient worker, string taskId)
    {
        _worker = worker;
        _taskId = taskId;
        _conflictTaskId = taskId;
    }

    /// <summary>Starts the conflict merge and loads the conflicted files as line-level segments.
    /// Returns true when there is something to resolve (caller should show the dialog).</summary>
    public async Task<bool> OpenAsync(string targetBranch)
    {
        IsBusy = true;
        Error = null;
        try
        {
            var start = await _worker.StartConflictMergeAsync(_taskId, targetBranch);
            if (!string.Equals(start.Status, "conflict", StringComparison.Ordinal))
            {
                // Any non-conflict outcome here means nothing was left mid-merge for this dialog
                // to resolve -- surface why instead of quietly closing with no explanation.
                if (start.Status is "blocked" or "untracked_collision")
                    Error = start.ErrorMessage;
                return false;
            }
            return await LoadDocumentsAsync();
        }
        catch (Exception ex)
        {
            Error = ex.Message;
            return false;
        }
        finally { IsBusy = false; }
    }

    /// <summary>Resolves a planning unit-merge conflict for <paramref name="subtaskId"/>. The merge is
    /// already mid-conflict (driven by the orchestrator), so this only loads the conflicted files;
    /// continue/abort hand back to the orchestrator on <paramref name="planningParentId"/>.</summary>
    public async Task<bool> OpenForPlanningAsync(string planningParentId, string subtaskId)
    {
        _planningParentId = planningParentId;
        _conflictTaskId = subtaskId;
        IsBusy = true;
        Error = null;
        try
        {
            return await LoadDocumentsAsync();
        }
        catch (Exception ex)
        {
            Error = ex.Message;
            return false;
        }
        finally { IsBusy = false; }
    }

    private async Task<bool> LoadDocumentsAsync()
    {
        var docs = await _worker.GetMergeConflictDocumentsAsync(_conflictTaskId);
        Files.Clear();
        _flat.Clear();
        foreach (var f in docs.Files)
        {
            var segments = f.Segments.Select(s => s.IsConflict
                ? MergeFileSegment.FromConflict(Hook(new MergeConflictBlock(s.Ours, s.Base, s.Theirs)))
                : MergeFileSegment.Stable(s.Text)).ToList();
            var file = new MergeFile(f.Path, f.IsBinary, segments);
            Files.Add(file);
            foreach (var c in file.Conflicts) _flat.Add((file, c));
        }

        OnPropertyChanged(nameof(TotalConflicts));
        OnPropertyChanged(nameof(BinaryFilePaths));
        OnPropertyChanged(nameof(HasBinaryFiles));
        OnPropertyChanged(nameof(HasMultipleFiles));
        OnPropertyChanged(nameof(FilesSummary));
        OnPropertyChanged(nameof(ContinueHint));
        RecomputeCanContinue();
        if (_flat.Count > 0)
            MoveTo(0); // also sets ActiveFile via MoveTo
        else if (Files.Count > 0)
            ActiveFile = Files[0];
        return Files.Count > 0;
    }

    private MergeConflictBlock Hook(MergeConflictBlock block)
    {
        block.PropertyChanged += OnBlockChanged;
        return block;
    }

    private void OnBlockChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName is nameof(MergeConflictBlock.IsResolved) or nameof(MergeConflictBlock.Resolution))
        {
            RecomputeCanContinue();
            OnPropertyChanged(nameof(ResolvedCount));
            OnPropertyChanged(nameof(PositionText));
            OnPropertyChanged(nameof(ActiveResultText));
            OnPropertyChanged(nameof(FilesSummary));
            OnPropertyChanged(nameof(ContinueHint));
        }
    }

    private void RecomputeCanContinue() =>
        CanContinue = Files.Count > 0 && Files.All(f => f.AllResolved);

    private void MoveTo(int index)
    {
        CurrentIndex = index;
        Current = _flat[index].Block;
        ActiveFile = _flat[index].File;
        OnPropertyChanged(nameof(CurrentPath));
        CurrentChanged?.Invoke();
    }

    [RelayCommand]
    private void SelectFile(MergeFile file)
    {
        // Jump to the first conflict in this file (if any); otherwise just switch the active file.
        var idx = _flat.FindIndex(x => x.File == file);
        if (idx >= 0)
            MoveTo(idx);
        else
            ActiveFile = file;
    }

    private bool CanGoNext() => CurrentIndex >= 0 && CurrentIndex < _flat.Count - 1;
    private bool CanGoPrevious() => CurrentIndex > 0;

    [RelayCommand(CanExecute = nameof(CanGoNext))]
    private void Next() => MoveTo(CurrentIndex + 1);

    [RelayCommand(CanExecute = nameof(CanGoPrevious))]
    private void Previous() => MoveTo(CurrentIndex - 1);

    [RelayCommand]
    private async Task ContinueAsync()
    {
        if (!CanContinue) return;
        IsBusy = true;
        Error = null;
        try
        {
            foreach (var file in Files.Where(f => !f.IsBinary))
                await _worker.WriteConflictResolutionAsync(_conflictTaskId, file.Path, file.Compose());

            if (_planningParentId is not null)
            {
                // Hand back to the orchestrator: it commits this subtask and drains the rest.
                // A later subtask conflict re-opens this editor via the PlanningMergeConflict broadcast.
                await _worker.ContinuePlanningMergeAsync(_planningParentId);
                CloseRequested?.Invoke();
                return;
            }

            var result = await _worker.ContinueConflictMergeAsync(_taskId);
            if (string.Equals(result.Status, "merged", StringComparison.Ordinal))
                CloseRequested?.Invoke();
            else
                Error = result.ErrorMessage ?? "Conflicts not fully resolved — review and retry.";
        }
        catch (Exception ex)
        {
            Error = ex.Message;
        }
        finally { IsBusy = false; }
    }

    [RelayCommand]
    private async Task AbortAsync()
    {
        IsBusy = true;
        try
        {
            if (_planningParentId is not null)
                await _worker.AbortPlanningMergeAsync(_planningParentId);
            else
                await _worker.AbortConflictMergeAsync(_taskId);
        }
        catch (Exception ex) { Error = ex.Message; }
        finally
        {
            IsBusy = false;
            CloseRequested?.Invoke();
        }
    }
}
