package pulls

import (
	"context"
	"errors"
	"fmt"
	"html/template"
	"io"
	"log/slog"
	"net/http"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"golang.org/x/sync/errgroup"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/pages"
	gitmirrorv1 "tangled.org/core/gitmirror/proto/gen"
)

// htmx fragment. render diff between commits
func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "PullDiffFragment")
	ctx := r.Context()

	var (
		baseRepoRaw = r.URL.Query().Get("baseRepo")
		headRepoRaw = r.URL.Query().Get("headRepo")
		base        = r.URL.Query().Get("base") // base commit SHA
		head        = r.URL.Query().Get("head") // head commit SHA
		unified     = r.URL.Query().Get("view") == "unified"
	)
	baseRepo, err := syntax.ParseDID(baseRepoRaw)
	if err != nil {
		if err := s.pages.PullDiffFragment(w, pages.PullDiffFragmentParams{
			ErrorMsg: "invalid base repository DID",
		}); err != nil {
			l.Error("failed to render", "err", err)
		}
		return
	}
	headRepo, err := syntax.ParseDID(headRepoRaw)
	if err != nil {
		if err := s.pages.PullDiffFragment(w, pages.PullDiffFragmentParams{
			ErrorMsg: "invalid head repository DID",
		}); err != nil {
			l.Error("failed to render", "err", err)
		}
		return
	}
	l.Debug("diff fragment", "baseRepo", baseRepo, "base", base, "headRepo", headRepo, "head", head)

	var params pages.PullDiffFragmentParams
	params.BaseRepo = baseRepo
	params.HeadRepo = headRepo
	params.DiffBase = base
	params.DiffHead = head
	params.DiffUrl = r.URL.Path
	params.Unified = unified
	params.Files, params.ErrorMsg = s.diffFragmentParams(ctx, l, baseRepo, base, headRepo, head, unified)
	if err := s.pages.PullDiffFragment(w, params); err != nil {
		l.Error("failed to render", "err", err)
	}
}

func (s *Pulls) diffFragmentParams(ctx context.Context, l *slog.Logger, baseRepo syntax.DID, base string, headRepo syntax.DID, head string, unified bool) ([]pages.DiffFile, string) {
	var err error
	if base != "" && !models.IsHash(base) {
		l := l.With("rev", base, "repo", baseRepo)
		if base, err = s.resolveRev(ctx, baseRepo, base); err != nil {
			l.Error("failed to resolve base revision", "err", err)
			return nil, "Failed to resolve base revision."
		}
	}
	if head != "" && !models.IsHash(head) {
		l := l.With("rev", head, "repo", headRepo)
		if head, err = s.resolveRev(ctx, headRepo, head); err != nil {
			l.Error("failed to resolve head revision", "err", err)
			return nil, "Failed to resolve head revision."
		}
	}

	// a. drain the diff stream into one fileDiff per changed file.
	req := &gitmirrorv1.DiffRequest{
		Head: &gitmirrorv1.RepoCommit{Repo: headRepo.String(), Commit: []byte(head)},
	}
	if base != "" {
		req.Base = &gitmirrorv1.RepoCommit{Repo: baseRepo.String(), Commit: []byte(base)}
	}
	stream, err := s.gitmirror.Diff(ctx, req)
	if err != nil {
		l.Error("failed to diff", "err", err)
		return nil, "Failed to diff. Try again later."
	}
	var files []*fileDiff
	for {
		fd, err := stream.Recv()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			l.Error("failed to drain diff response", "err", err)
			return nil, "Failed to diff. Try again later."
		}
		files = append(files, &fileDiff{diff: fd})
	}

	// b. fetch each file's base/head blob in parallel and split into lines.
	g, gctx := errgroup.WithContext(ctx)
	for _, f := range files {
		g.Go(func() error {
			lhs, rhs := f.diff.GetLhsSrc(), f.diff.GetRhsSrc()
			// Binary/submodule files have no line content to fetch.
			if isBinaryOrSubmodule(lhs) || isBinaryOrSubmodule(rhs) {
				return nil
			}
			baseBlob, err := s.getBlob(gctx, baseRepo, lhs.GetOid())
			if err != nil {
				return err
			}
			headBlob, err := s.getBlob(gctx, headRepo, rhs.GetOid())
			if err != nil {
				return err
			}

			// TODO: highlight each blobs
			// pass lhs_positions & rhs_positions so highlighter can apply diff highlights
			for line := range strings.SplitSeq(strings.TrimSuffix(string(baseBlob), "\n"), "\n") {
				f.baseLines = append(f.baseLines, template.HTML(fmt.Sprintf("<div><span>%s</span></div>", template.HTMLEscapeString(line))))
			}
			for line := range strings.SplitSeq(strings.TrimSuffix(string(headBlob), "\n"), "\n") {
				f.headLines = append(f.headLines, template.HTML(fmt.Sprintf("<div><span>%s</span></div>", template.HTMLEscapeString(line))))
			}

			// f.baseLines = strings.Split(strings.TrimSuffix(string(baseBlob), "\n"), "\n")
			// f.headLines = strings.Split(strings.TrimSuffix(string(headBlob), "\n"), "\n")
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		l.Error("failed to prepare html", "err", err)
		return nil, "Failed to render diff. Try again later."
	}

	// c. build the render model
	var outFiles []pages.DiffFile
	for _, f := range files {
		df := pages.DiffFile{Path: f.diff.GetRhsSrc().GetPath()}

		if isBinaryOrSubmodule(f.diff.GetLhsSrc()) || isBinaryOrSubmodule(f.diff.GetRhsSrc()) {
			df.Note = "binary or submodule"
			outFiles = append(outFiles, df)
			continue
		}

		for _, h := range buildHunks(f.baseLines, f.headLines, f.diff.Hunks) {
			var dh pages.DiffHunk
			if unified {
				dh.Lines = buildUnifiedLines(h, f.baseLines, f.headLines)
			} else {
				dh.Rows = buildSplitRows(h, f.baseLines, f.headLines)
			}
			df.Hunks = append(df.Hunks, dh)
		}
		outFiles = append(outFiles, df)
	}

	return outFiles, ""
}
