package workspace

import (
	"crypto/sha256"
	"encoding/hex"
	"hash"
	"os"

	"github.com/go-git/go-git/v5/plumbing/object"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// branchFileTokenInputs is the structured input to the branch per-file token
// digest. The encoder writes a versioned domain tag followed by fields in a
// fixed order, each length-prefixed, so equal inputs always produce equal
// digests regardless of map iteration or field-ordering drift — and so
// values containing newlines, spaces, or NUL bytes cannot collide with a
// different field split.
//
// The digest is AUTHORITATIVE for branch file content: equal digests imply
// byte-equal diff content for this file (path, change type, both blob hashes,
// both modes). Path and OldPath are part of the input so a rename (R) where
// only the path differs produces a different token from a modify (M) at the
// same content.
//
// Inputs deliberately exclude symbolic refs and mergeBase — those can move
// independently of the underlying commits and would weaken the equality
// guarantee.
type branchFileTokenInputs struct {
	Path        string
	OldPath     string
	ChangeType  string
	OldBlobHash string
	OldMode     string
	NewBlobHash string
	NewMode     string
}

func (i branchFileTokenInputs) writeTo(h hash.Hash) {
	// Field order MUST stay stable. Reviewers and tests assert on this order;
	// do not reorder without bumping the domain tag version. Every field uses
	// the length-prefix helpers (see fingerprint_token_encoder.go) so values
	// cannot be reinterpreted as a different field split.
	//
	// Blob pair comes before mode pair so the byte stream groups
	// content-identity fields together; the locked schema is
	// (path, oldPath, changeType, oldBlob, newBlob, oldMode, newMode).
	writeTokenString(h, branchFileTokenDomainV1)
	writeTokenString(h, i.Path)
	writeTokenString(h, i.OldPath)
	writeTokenString(h, i.ChangeType)
	writeTokenString(h, i.OldBlobHash)
	writeTokenString(h, i.NewBlobHash)
	writeTokenString(h, i.OldMode)
	writeTokenString(h, i.NewMode)
}

// workingFileTokenInputs is the structured input to the working per-file token
// digest. The digest is NON-AUTHORITATIVE (hint strength): it covers all
// three sides of the working diff (HEAD, index, worktree stat), but mtime is
// involved, so equal digests strongly suggest no change without guaranteeing
// it. The per-side Missing flags ensure a side disappearing produces a
// different digest from one where the side is present with the same content.
//
// Inputs deliberately exclude filesystem path strings — only the path-as-key
// and the stat/blob values participate. The digest must not leak filesystem
// layout beyond what the cache key (canonical repo root) already exposes.
type workingFileTokenInputs struct {
	Path       string
	OldPath    string
	ChangeType string

	HeadMissing  bool
	HeadBlobHash string
	HeadMode     string

	IndexMissing  bool
	IndexBlobHash string
	IndexMode     string

	WtMissing bool
	WtSize    int64
	WtMtimeNS int64
	WtMode    string
	WtInode   uint64
}

func (i workingFileTokenInputs) writeTo(h hash.Hash) {
	// See branchFileTokenInputs.writeTo for the encoding invariant: every
	// field goes through the length-prefix helpers so a missing side (or any
	// blob hash / mode string containing whitespace) cannot be reinterpreted
	// as a different field split.
	writeTokenString(h, workingFileTokenDomainV2)
	writeTokenString(h, i.Path)
	writeTokenString(h, i.OldPath)
	writeTokenString(h, i.ChangeType)
	writeTokenBool(h, i.HeadMissing)
	writeTokenString(h, i.HeadBlobHash)
	writeTokenString(h, i.HeadMode)
	writeTokenBool(h, i.IndexMissing)
	writeTokenString(h, i.IndexBlobHash)
	writeTokenString(h, i.IndexMode)
	writeTokenBool(h, i.WtMissing)
	writeTokenInt64(h, i.WtSize)
	writeTokenInt64(h, i.WtMtimeNS)
	writeTokenString(h, i.WtMode)
	writeTokenUint64(h, i.WtInode)
}

// reviewBranchFileToken derives an opaque, AUTHORITATIVE per-file token for a
// branch comparison by hashing branchFileTokenInputs with SHA-256. The full
// 64-hex digest is returned (no truncation) so equal tokens guarantee
// byte-equal diff content.
//
// Callers MUST populate file.Path / file.OldPath / file.ChangeType before
// invoking this — the token covers all three so a path-only or
// change-type-only change produces a different digest.
func reviewBranchFileToken(file types.ReviewFile, from, to object.TreeEntry) string {
	inputs := branchFileTokenInputs{
		Path:       file.Path,
		OldPath:    file.OldPath,
		ChangeType: file.ChangeType,
	}
	if !from.Hash.IsZero() || from.Mode != 0 {
		inputs.OldBlobHash = from.Hash.String()
		inputs.OldMode = from.Mode.String()
	}
	if !to.Hash.IsZero() || to.Mode != 0 {
		inputs.NewBlobHash = to.Hash.String()
		inputs.NewMode = to.Mode.String()
	}
	digest := sha256.New()
	inputs.writeTo(digest)
	return hex.EncodeToString(digest.Sum(nil))
}

// reviewWorkingFileHintToken derives an opaque, NON-AUTHORITATIVE per-file
// token for a working file by hashing workingFileTokenInputs with SHA-256.
// The digest is truncated to reviewFingerprintTokenHexLen (16 hex / 64 bits)
// — enough collision resistance for hint-strength change detection across
// the small per-repo token space, while keeping wire cost compact.
//
// headEntry / indexEntry may be nil (or carry a zero hash) when the side is
// absent. The wt side is populated from os.Stat; a stat failure (e.g. the
// file was removed mid-scan) records the missing sentinel rather than
// failing the whole token.
func reviewWorkingFileHintToken(root string, file types.ReviewFile, headEntry, indexEntry *object.TreeEntry) string {
	// Do NOT TrimSpace the path: Git paths may legitimately contain leading
	// or trailing whitespace (or newlines), and trimming would remap the
	// token to a different file's stat — exactly the collision the
	// length-prefix encoder is supposed to prevent. Only check emptiness;
	// the raw path enters both the digest and the safeJoin.
	if file.Path == "" {
		return ""
	}

	inputs := workingFileTokenInputs{
		Path:       file.Path,
		OldPath:    file.OldPath,
		ChangeType: file.ChangeType,
	}
	if headEntry != nil && !headEntry.Hash.IsZero() {
		inputs.HeadBlobHash = headEntry.Hash.String()
		inputs.HeadMode = headEntry.Mode.String()
	} else {
		inputs.HeadMissing = true
	}
	if indexEntry != nil && !indexEntry.Hash.IsZero() {
		inputs.IndexBlobHash = indexEntry.Hash.String()
		inputs.IndexMode = indexEntry.Mode.String()
	} else {
		inputs.IndexMissing = true
	}

	target, err := safeJoin(root, file.Path)
	if err != nil {
		inputs.WtMissing = true
	} else {
		info, err := os.Stat(target)
		if err != nil {
			inputs.WtMissing = true
		} else {
			inputs.WtSize = info.Size()
			inputs.WtMtimeNS = info.ModTime().UnixNano()
			inputs.WtMode = info.Mode().String()
			inputs.WtInode = reviewWorkingFileInode(info)
		}
	}

	digest := sha256.New()
	inputs.writeTo(digest)
	return hex.EncodeToString(digest.Sum(nil))[:reviewFingerprintTokenHexLen]
}

// Compile-time assertion that we use hash.Hash so future refactors do not
// accidentally pass an io.Writer to writeTo and lose the Sum method.
var _ hash.Hash = (hash.Hash)(nil)
