package githttp

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/pingcap/agent-git-service/internal/service"
)

const lfsMediaType = "application/vnd.git-lfs+json"

type lfsBatchRequest struct {
	Operation string      `json:"operation"`
	Objects   []lfsObject `json:"objects"`
}

type lfsObject struct {
	OID     string               `json:"oid"`
	Size    int64                `json:"size"`
	Actions map[string]lfsAction `json:"actions,omitempty"`
	Error   *lfsObjectError      `json:"error,omitempty"`
}

type lfsAction struct {
	Href string `json:"href"`
}

type lfsObjectError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

// LFSBatch implements the Git LFS basic transfer batch negotiation endpoint.
func (h *Handler) LFSBatch(w http.ResponseWriter, r *http.Request) {
	var request lfsBatchRequest
	decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<20))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&request); err != nil {
		writeLFSJSON(w, http.StatusBadRequest, map[string]any{"message": "invalid LFS batch request"})
		return
	}
	required := service.RepoPermissionRead
	if request.Operation == "upload" {
		required = service.RepoPermissionWrite
		r = r.WithContext(service.ContextWithRepositoryWriteOperation(r.Context()))
	} else if request.Operation != "download" {
		writeLFSJSON(w, http.StatusUnprocessableEntity, map[string]any{"message": "unsupported LFS operation"})
		return
	}
	repoCtx, ok := h.resolveRepoContext(w, r, "lfs/batch", required)
	if !ok {
		return
	}
	response := make([]lfsObject, 0, len(request.Objects))
	for _, object := range request.Objects {
		entry := lfsObject{OID: strings.ToLower(strings.TrimSpace(object.OID)), Size: object.Size}
		if !validLFSOID(entry.OID) || entry.Size < 0 {
			entry.Error = &lfsObjectError{Code: http.StatusUnprocessableEntity, Message: "invalid LFS object"}
			response = append(response, entry)
			continue
		}
		path := lfsObjectPath(repoCtx.repoPath, entry.OID)
		available := validLFSObjectFile(path, entry.OID, entry.Size)
		href := lfsObjectURL(r, entry.OID)
		if request.Operation == "download" {
			if available {
				entry.Actions = map[string]lfsAction{"download": {Href: href}}
			} else {
				entry.Error = &lfsObjectError{Code: http.StatusNotFound, Message: "LFS object not found"}
			}
		} else if !available {
			entry.Actions = map[string]lfsAction{"upload": {Href: href}}
		}
		response = append(response, entry)
	}
	writeLFSJSON(w, http.StatusOK, map[string]any{"transfer": "basic", "objects": response})
}

// LFSDownload returns one content-addressed LFS object after repository read
// authorization. The stored file was checksum-verified at upload time.
func (h *Handler) LFSDownload(w http.ResponseWriter, r *http.Request) {
	repoCtx, ok := h.resolveRepoContext(w, r, "lfs/download", service.RepoPermissionRead)
	if !ok {
		return
	}
	oid := strings.ToLower(pathParam(r, "oid"))
	if !validLFSOID(oid) {
		http.NotFound(w, r)
		return
	}
	file, err := os.Open(lfsObjectPath(repoCtx.repoPath, oid))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer file.Close()
	stat, err := file.Stat()
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
	w.WriteHeader(http.StatusOK)
	_, _ = io.Copy(w, file)
}

// LFSUpload atomically stores one object only when its byte count and SHA-256
// match the content-addressed OID in the URL.
func (h *Handler) LFSUpload(w http.ResponseWriter, r *http.Request) {
	r = r.WithContext(service.ContextWithRepositoryWriteOperation(r.Context()))
	repoCtx, ok := h.resolveRepoContext(w, r, "lfs/upload", service.RepoPermissionWrite)
	if !ok {
		return
	}
	oid := strings.ToLower(pathParam(r, "oid"))
	if !validLFSOID(oid) {
		writeLFSJSON(w, http.StatusUnprocessableEntity, map[string]any{"message": "invalid LFS OID"})
		return
	}
	if r.ContentLength < 0 || r.ContentLength > maxPushBytes() {
		writeLFSJSON(w, http.StatusRequestEntityTooLarge, map[string]any{"message": "invalid LFS object size"})
		return
	}
	target := lfsObjectPath(repoCtx.repoPath, oid)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	temp, err := os.CreateTemp(filepath.Dir(target), ".lfs-upload-*")
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	tempPath := temp.Name()
	defer os.Remove(tempPath)
	hash := sha256.New()
	written, copyErr := io.Copy(io.MultiWriter(temp, hash), io.LimitReader(r.Body, maxPushBytes()+1))
	closeErr := temp.Close()
	if copyErr != nil || closeErr != nil || written != r.ContentLength || written > maxPushBytes() || hex.EncodeToString(hash.Sum(nil)) != oid {
		writeLFSJSON(w, http.StatusUnprocessableEntity, map[string]any{"message": "LFS object size or SHA-256 mismatch"})
		return
	}
	if err := os.Chmod(tempPath, 0o444); err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	if err := os.Rename(tempPath, target); err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
}

func lfsObjectPath(repositoryPath, oid string) string {
	return filepath.Join(repositoryPath, "lfs", "objects", oid[:2], oid[2:4], oid)
}

func validLFSOID(oid string) bool {
	if len(oid) != 64 {
		return false
	}
	_, err := hex.DecodeString(oid)
	return err == nil
}

func validLFSObjectFile(path, oid string, size int64) bool {
	file, err := os.Open(path)
	if err != nil {
		return false
	}
	defer file.Close()
	hash := sha256.New()
	written, err := io.Copy(hash, file)
	return err == nil && written == size && hex.EncodeToString(hash.Sum(nil)) == oid
}

func lfsObjectURL(r *http.Request, oid string) string {
	scheme := "http"
	if forwarded := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Proto"), ",")[0]); forwarded == "http" || forwarded == "https" {
		scheme = forwarded
	} else if r.TLS != nil {
		scheme = "https"
	}
	base := strings.TrimSuffix(r.URL.Path, "/objects/batch")
	return fmt.Sprintf("%s://%s%s/objects/%s", scheme, r.Host, base, oid)
}

func writeLFSJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", lfsMediaType)
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(value)
}
