package xrpc

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os/exec"
	"strings"

	"github.com/bluesky-social/indigo/atproto/atclient"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"tangled.org/core/api/tangled"
)

func (x *Xrpc) KeepCommit(w http.ResponseWriter, r *http.Request) {
	var input tangled.GitKeepCommit_Input
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"})
		return
	}

	if err := gitKeepCommit_Input_Validate(input); err != nil {
		writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: err.Error()})
		return
	}
	if syntax.ATURI(input.Record).RecordKey() == "" {
		writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "record at-uri should have rkey"})
		return
	}

	output, status, apierr := x.keepCommit(r.Context(), input)
	if apierr != nil {
		writeJson(w, status, apierr)
		return
	}
	writeJson(w, status, output)
}

func (x *Xrpc) keepCommit(ctx context.Context, input tangled.GitKeepCommit_Input) (*tangled.GitKeepCommit_Output, int, *atclient.ErrorBody) {
	repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Repo)
	if err != nil {
		return nil, http.StatusNotFound, &atclient.ErrorBody{Name: "RepoNotFound", Message: fmt.Sprintf("unknown repository: %q", input.Repo)}
	}

	record := syntax.ATURI(input.Record)
	recordIdent, err := x.Resolver.Directory().Lookup(ctx, record.Authority())
	if err != nil {
		return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "Failed to resolve record authority"}
	}
	recordDID := recordIdent.DID

	var commitID string

	switch {
	case input.Source.GitKeepCommit_Commit != nil:
		source := input.Source.GitKeepCommit_Commit
		if input.Repo == source.Repo {
			// no-op. we already have that commit
		} else {
			// TODO: target repo should own the source commit
			return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "source repo should match the target repo"}
			// // fetch commit from source repo
			// if err := x.fetchCommitFrom(ctx, syntax.DID(source.Repo), source.Oid); err != nil {
			// 	return nil, http.StatusInternalServerError, &atclient.ErrorBody{Name: "CommitNotFound", Message: "Failed to fetch commit from source repo"}
			// }
		}
		commitID = source.Oid

	case input.Source.GitKeepCommit_Patches != nil:
		// TODO: apply patches to target commit and bring tip commit ID
		return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "patches source is not supported"}

	default:
		return nil, http.StatusBadRequest, &atclient.ErrorBody{Name: "BadRequest", Message: "source should be one of: [commit, patches]"}
	}

	// create refs/tngl/keep/{did}/{collection}/{rkey}/{oid}
	refName := EscapeGitRef(fmt.Sprintf("refs/tngl/keep/%s/%s/%s/%s", recordDID.String(), record.Collection(), record.RecordKey(), commitID))
	cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "update-ref", refName, commitID)
	if out, err := cmd.CombinedOutput(); err != nil {
		x.Logger.Error("failed to keep commit", "err", err, "out", string(out))
		return nil, http.StatusInternalServerError, &atclient.ErrorBody{Name: "InternalServerError", Message: "Failed to keep commit"}
	}

	return &tangled.GitKeepCommit_Output{
		Commit: commitID,
	}, http.StatusOK, nil
}

// lexgen doesn't give Validate() method...
func gitKeepCommit_Input_Validate(input tangled.GitKeepCommit_Input) error {
	if _, err := syntax.ParseDID(input.Repo); err != nil {
		return fmt.Errorf("repo: invalid repo DID: %w", err)
	}
	if _, err := syntax.ParseATURI(input.Record); err != nil {
		return fmt.Errorf("repo: invalid record at-uri: %w", err)
	}
	switch {
	case input.Source.GitKeepCommit_Commit != nil:
		if _, err := syntax.ParseDID(input.Source.GitKeepCommit_Commit.Repo); err != nil {
			return fmt.Errorf("source: commit: invalid repo DID: %w", err)
		}
		if ok := IsHash(input.Source.GitKeepCommit_Commit.Oid); !ok {
			return fmt.Errorf("source: commit: invalid commit OID: %q", input.Source.GitKeepCommit_Commit.Oid)
		}
	case input.Source.GitKeepCommit_Patches != nil:
		return fmt.Errorf("source: patches: patch is not supported yet")
		// for i, patch := range input.Source.GitKeepCommit_Patches.Patches {
		// 	if err := validatePatch(patch); err != nil {
		// 		return fmt.Errorf("source: patches: invalid patches at [%d]: %w", i, err)
		// 	}
		// }
	default:
		return fmt.Errorf("source should be one of: [commit, patches]")
	}
	return nil
}

func EscapeGitRef(s string) string {
	var b strings.Builder
	b.Grow(len(s) * 4 / 3)
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c >= 'a' && c <= 'z') ||
			(c >= 'A' && c <= 'Z') ||
			(c >= '0' && c <= '9') ||
			c == '/' || c == '-' || c == '_' || c == '.' {
			b.WriteByte(c)
			continue
		}
		b.WriteByte('%')
		b.WriteByte("0123456789ABCDEF"[c>>4])
		b.WriteByte("0123456789ABCDEF"[c&15])
	}
	return strings.ToLower(b.String())
}

func IsHash(s string) bool {
	switch len(s) {
	case 40: // SHA1
	case 64: // SHA2
	default:
		return false
	}
	_, err := hex.DecodeString(s)
	return err == nil
}
