using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Threading;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
using AvaloniaEdit.Rendering;
using AvaloniaEdit.TextMate;
using ClaudeDo.Ui.ViewModels.Conflicts;
using ClaudeDo.Ui.Views.Controls;
using Seg = ClaudeDo.Ui.Views.Controls.DiffEditorSetup.Seg;

namespace ClaudeDo.Ui.Views.Conflicts;

public partial class ConflictResolverView : Window
{
    private ConflictResolverViewModel? _vm;
    private TextMate.Installation? _oursTm, _resultTm, _theirsTm;
    private bool _editorsReady;

    // Fixed conflict spans for the read-only side panes (recomputed each rebuild).
    private List<(int Offset, int Length, MergeConflictBlock Block)> _oursSpans = new();
    private List<(int Offset, int Length, MergeConflictBlock Block)> _theirsSpans = new();

    // Live, edit-tracked conflict regions in the editable result document.
    private readonly List<ResultRegion> _resultRegions = new();
    private readonly List<MergeConflictBlock> _hookedBlocks = new();

    private bool _rebuilding;
    private bool _applyingAccept;
    private bool _gutterPending;
    private int _gutterRetries;

    public ConflictResolverView()
    {
        InitializeComponent();
    }

    protected override void OnDataContextChanged(EventArgs e)
    {
        base.OnDataContextChanged(e);

        if (_vm is not null)
        {
            _vm.ActiveFileChanged -= Rebuild;
            _vm.CurrentChanged -= ScrollToCurrent;
        }
        _vm = DataContext as ConflictResolverViewModel;
        if (_vm is null) return;

        _vm.CloseRequested = Close;
        EnsureEditors();
        _vm.ActiveFileChanged += Rebuild;
        _vm.CurrentChanged += ScrollToCurrent;
        Rebuild();
    }

    // ── One-time editor setup ────────────────────────────────────────────────

    private void EnsureEditors()
    {
        if (_editorsReady) return;
        _editorsReady = true;
        _oursTm   = DiffEditorSetup.InstallHighlighting(OursEditor);
        _resultTm = DiffEditorSetup.InstallHighlighting(ResultEditor);
        _theirsTm = DiffEditorSetup.InstallHighlighting(TheirsEditor);

        ResultEditor.Document ??= new TextDocument();
        ResultEditor.Document.Changed += OnResultDocumentChanged;
        ResultEditor.TextArea.ReadOnlySectionProvider =
            new ConflictReadOnlyProvider(() => _resultRegions.Select(r => (r.Start.Offset, r.End.Offset)));

        // Panes are whole files here, not aligned rows, so raw pixel offsets line up and the
        // gutter buttons only have to be repositioned once per scroll.
        new VerticalScrollSync(new[] { OursEditor, ResultEditor, TheirsEditor },
                               afterSync: PositionGutters);

        var conflict = DiffEditorSetup.Brush(this, "MergeConflictTintBrush", Color.Parse("#28C87060"));
        var resolved = DiffEditorSetup.Brush(this, "MergeResolvedTintBrush", Color.Parse("#206FA86B"));
        OursEditor.TextArea.TextView.BackgroundRenderers.Add(new MergeBlockRenderer(
            () => _oursSpans.Select(s => (s.Offset, s.Length, s.Block.IsResolved)), conflict, resolved));
        ResultEditor.TextArea.TextView.BackgroundRenderers.Add(new MergeBlockRenderer(
            () => _resultRegions.Select(r => (r.Start.Offset, r.End.Offset - r.Start.Offset, r.Block.IsResolved)), conflict, resolved));
        TheirsEditor.TextArea.TextView.BackgroundRenderers.Add(new MergeBlockRenderer(
            () => _theirsSpans.Select(s => (s.Offset, s.Length, s.Block.IsResolved)), conflict, resolved));
    }

    // ── Rebuild the three documents for the active file ───────────────────────

    private void Rebuild()
    {
        if (_vm is null) return;
        _rebuilding = true;
        _gutterRetries = 0; // fresh retry budget for this file's gutter layout
        try
        {
            ClearGutters();
            UnhookBlocks();
            _resultRegions.Clear();

            var file = _vm.ActiveFile;
            if (file is null || file.IsBinary)
            {
                OursEditor.Text = TheirsEditor.Text = "";
                if (ResultEditor.Document is { } d0) d0.Text = "";
                _oursSpans = new(); _theirsSpans = new();
                InvalidateRenderers();
                return;
            }

            var (oursText, oursSpans) = BuildSide(file, b => b.Ours);
            var (theirsText, theirsSpans) = BuildSide(file, b => b.Theirs);
            // Unresolved conflicts start EMPTY — the user builds the result by appending sides.
            var (resultText, resultSpans) = BuildSide(file, b => b.Resolution ?? "");
            _oursSpans = oursSpans;
            _theirsSpans = theirsSpans;

            OursEditor.Text = oursText;
            TheirsEditor.Text = theirsText;
            ResultEditor.Document ??= new TextDocument();
            ResultEditor.Document.Text = resultText;

            var doc = ResultEditor.Document;
            foreach (var (offset, length, block) in resultSpans)
            {
                var start = doc.CreateAnchor(offset);
                start.MovementType = AnchorMovementType.BeforeInsertion;
                var end = doc.CreateAnchor(offset + length);
                end.MovementType = AnchorMovementType.AfterInsertion;
                _resultRegions.Add(new ResultRegion(block, start, end));
                block.PropertyChanged += OnBlockChanged;
                _hookedBlocks.Add(block);
            }

            DiffEditorSetup.ApplyGrammar(file.Path, _oursTm, _resultTm, _theirsTm);
            InvalidateRenderers();
        }
        finally { _rebuilding = false; }

        QueueGutters();
    }

    private static (string Text, List<(int Offset, int Length, MergeConflictBlock Block)> Spans) BuildSide(
        MergeFile file, Func<MergeConflictBlock, string> pick)
    {
        var sb = new StringBuilder();
        var spans = new List<(int, int, MergeConflictBlock)>();
        foreach (var seg in file.Segments)
        {
            if (seg.IsConflict)
            {
                var text = pick(seg.Conflict!);
                spans.Add((sb.Length, text.Length, seg.Conflict!));
                sb.Append(text);
            }
            else
            {
                sb.Append(seg.StableText);
            }
        }
        return (sb.ToString(), spans);
    }

    private void UnhookBlocks()
    {
        foreach (var b in _hookedBlocks) b.PropertyChanged -= OnBlockChanged;
        _hookedBlocks.Clear();
    }

    private void OnBlockChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName is nameof(MergeConflictBlock.IsResolved) or nameof(MergeConflictBlock.Resolution))
        {
            InvalidateRenderers();
            QueueGutters();
        }
    }

    // ── User edits in the result document flow back to the owning conflict ────

    private void OnResultDocumentChanged(object? sender, DocumentChangeEventArgs e)
    {
        if (_rebuilding || _applyingAccept) return;
        foreach (var r in _resultRegions)
        {
            if (e.Offset >= r.Start.Offset && e.Offset <= r.End.Offset)
            {
                r.Block.Resolution = ResultEditor.Document.GetText(r.Start.Offset, Math.Max(0, r.End.Offset - r.Start.Offset));
                break;
            }
        }
        QueueGutters();
    }

    // ── Toggle a side in/out of the result region ────────────────────────────

    // Each side can be included at most once. Clicking adds it (in click order, first on
    // top); clicking again removes it. The region content is rebuilt from the included set.
    private void ToggleSide(ResultRegion region, char side)
    {
        if (region.Order.Contains(side)) region.Order.Remove(side);
        else region.Order.Add(side);

        var text = string.Concat(region.Order.Select(c => c == 'o' ? region.Block.Ours : region.Block.Theirs));
        _applyingAccept = true;
        try { ResultEditor.Document.Replace(region.Start.Offset, region.End.Offset - region.Start.Offset, text); }
        finally { _applyingAccept = false; }

        region.Block.Resolution = region.Order.Count == 0 ? null : text;
        InvalidateRenderers();
        PositionGutters();
    }

    // ── Inline accept controls in the between-pane gutters ────────────────────

    private void ClearGutters()
    {
        LeftGutter.Children.Clear();
        RightGutter.Children.Clear();
    }

    // Coalesce gutter re-layouts so repeated change/scroll events can't flood the dispatcher.
    private void QueueGutters()
    {
        if (_gutterPending) return;
        _gutterPending = true;
        Dispatcher.UIThread.Post(() => { _gutterPending = false; PositionGutters(); }, DispatcherPriority.Background);
    }

    private void PositionGutters()
    {
        ClearGutters();
        PopulateConflictMap();
        if (_vm?.ActiveFile is null) return;
        var tv = ResultEditor.TextArea.TextView;
        if (!tv.VisualLinesValid)
        {
            // Retry until the editor is laid out, but bounded so a never-laid-out editor
            // (e.g. minimized window) can't busy-loop the dispatcher.
            if (_gutterRetries++ < 40) QueueGutters();
            return;
        }
        _gutterRetries = 0;

        var doc = ResultEditor.Document;
        foreach (var region in _resultRegions)
        {
            // Controls stay visible whether or not a side is included, so either can be toggled.
            var len = region.End.Offset - region.Start.Offset;
            ISegment probe = len > 0
                ? new Seg(region.Start.Offset, len)
                : new Seg(region.Start.Offset, region.Start.Offset < doc.TextLength ? 1 : 0);
            var rects = BackgroundGeometryBuilder.GetRectsForSegment(tv, probe).ToList();
            if (rects.Count == 0) continue;
            var y = rects[0].Top;

            var r = region;
            var oursIn = region.Order.Contains('o');
            var theirsIn = region.Order.Contains('t');

            if (tv.TranslatePoint(new Point(0, y), LeftGutter) is { } pl &&
                pl.Y > -24 && pl.Y < LeftGutter.Bounds.Height + 24)
                AddAcceptButton(LeftGutter, pl.Y, oursIn ? "−" : "›", () => ToggleSide(r, 'o'),
                    Tr(oursIn ? "conflictResolver.removeOurs" : "conflictResolver.acceptOurs"));

            if (tv.TranslatePoint(new Point(0, y), RightGutter) is { } pr &&
                pr.Y > -24 && pr.Y < RightGutter.Bounds.Height + 24)
                AddAcceptButton(RightGutter, pr.Y, theirsIn ? "−" : "‹", () => ToggleSide(r, 't'),
                    Tr(theirsIn ? "conflictResolver.removeTheirs" : "conflictResolver.acceptTheirs"));
        }
    }

    private void AddAcceptButton(Canvas canvas, double y, string glyph, Action onClick, string tip)
    {
        var b = new Button { Content = glyph };
        b.Classes.Add("accept-gutter");
        ToolTip.SetTip(b, tip);
        b.Click += (_, _) => onClick();
        Canvas.SetLeft(b, 1);
        Canvas.SetTop(b, Math.Max(0, y));
        canvas.Children.Add(b);
    }

    // ── Conflict overview ruler (right of the result pane) ───────────────────

    // A proportional map of every conflict in the active file so they're findable in
    // long files without scrolling; ticks recolor by resolved state and jump on click.
    private void PopulateConflictMap()
    {
        ConflictMap.Children.Clear();
        if (_vm?.ActiveFile is null || _resultRegions.Count == 0) return;
        var h = ConflictMap.Bounds.Height;
        if (h <= 1) return;
        var doc = ResultEditor.Document;
        var totalLines = Math.Max(1, doc.LineCount);
        var unresolved = DiffEditorSetup.Brush(this, "MergeConflictEdgeBrush", Color.Parse("#80C87060"));
        var resolved = DiffEditorSetup.Brush(this, "MergeResolvedEdgeBrush", Color.Parse("#806FA86B"));

        foreach (var region in _resultRegions)
        {
            var line = doc.GetLineByOffset(region.Start.Offset).LineNumber;
            var y = (line - 1) / (double)totalLines * h;
            var tick = new Rectangle
            {
                Width = 9,
                Height = 4,
                Fill = region.Block.IsResolved ? resolved : unresolved,
                Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand),
            };
            Canvas.SetLeft(tick, 2);
            Canvas.SetTop(tick, Math.Min(h - 4, Math.Max(0, y)));
            var r = region;
            tick.PointerPressed += (_, _) => JumpToRegion(r);
            ConflictMap.Children.Add(tick);
        }
    }

    private void JumpToRegion(ResultRegion region)
    {
        var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
        ResultEditor.ScrollToLine(line);
        QueueGutters();
    }

    private static string Tr(string key) => ClaudeDo.Ui.Localization.Loc.T(key);

    private void ScrollToCurrent()
    {
        if (_vm?.Current is not { } block) return;
        var region = _resultRegions.FirstOrDefault(r => ReferenceEquals(r.Block, block));
        if (region is null) return;
        var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
        ResultEditor.ScrollToLine(line);
        QueueGutters();
    }

    private void InvalidateRenderers()
    {
        OursEditor.TextArea.TextView.InvalidateVisual();
        ResultEditor.TextArea.TextView.InvalidateVisual();
        TheirsEditor.TextArea.TextView.InvalidateVisual();
    }

    // ── Helper types (single-consumer; live with their consumer per repo style) ─

    /// <summary>An editable conflict region in the result document, tracking which sides are
    /// currently included (in click order — <c>'o'</c> = ours/main, <c>'t'</c> = theirs/incoming).</summary>
    private sealed class ResultRegion
    {
        public ResultRegion(MergeConflictBlock block, TextAnchor start, TextAnchor end)
        {
            Block = block; Start = start; End = end;
        }
        public MergeConflictBlock Block { get; }
        public TextAnchor Start { get; }
        public TextAnchor End { get; }
        public List<char> Order { get; } = new();
    }

    /// <summary>Paints each conflict block with the unresolved/resolved tint across a pane.</summary>
    private sealed class MergeBlockRenderer : IBackgroundRenderer
    {
        private readonly Func<IEnumerable<(int Offset, int Length, bool Resolved)>> _spans;
        private readonly IBrush _conflict;
        private readonly IBrush _resolved;

        public MergeBlockRenderer(Func<IEnumerable<(int, int, bool)>> spans, IBrush conflict, IBrush resolved)
        {
            _spans = spans; _conflict = conflict; _resolved = resolved;
        }

        public KnownLayer Layer => KnownLayer.Background;

        public void Draw(TextView textView, DrawingContext drawingContext)
        {
            if (!textView.VisualLinesValid) return;
            foreach (var (offset, length, resolved) in _spans())
            {
                var brush = resolved ? _resolved : _conflict;
                if (length > 0)
                {
                    var builder = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 2 };
                    builder.AddSegment(textView, new Seg(offset, length));
                    var geo = builder.CreateGeometry();
                    if (geo is not null) drawingContext.DrawGeometry(brush, null, geo);
                }
                else
                {
                    // Empty region (nothing accepted yet): a thin marker bar marks the spot.
                    var at = offset < textView.Document.TextLength ? offset : Math.Max(0, offset - 1);
                    var rects = BackgroundGeometryBuilder.GetRectsForSegment(textView, new Seg(at, 1)).ToList();
                    if (rects.Count > 0)
                        drawingContext.FillRectangle(brush, new Rect(0, rects[0].Top, textView.Bounds.Width, 3));
                }
            }
        }
    }

    /// <summary>Makes everything read-only except the live conflict regions in the result document.</summary>
    private sealed class ConflictReadOnlyProvider : IReadOnlySectionProvider
    {
        private readonly Func<IEnumerable<(int Start, int End)>> _regions;
        public ConflictReadOnlyProvider(Func<IEnumerable<(int, int)>> regions) => _regions = regions;

        public bool CanInsert(int offset) => _regions().Any(r => offset >= r.Start && offset <= r.End);

        public IEnumerable<ISegment> GetDeletableSegments(ISegment segment)
        {
            foreach (var (start, end) in _regions())
            {
                var s = Math.Max(segment.Offset, start);
                var e = Math.Min(segment.EndOffset, end);
                if (e > s) yield return new Seg(s, e - s);
            }
        }
    }
}
