package git

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
)

// CredentialKind 与 DB 列 git_credentials.kind 完全一致。
type CredentialKind string

const (
	CredentialPAT    CredentialKind = "pat"
	CredentialSSHKey CredentialKind = "ssh_key"
	CredentialNone   CredentialKind = "none"
)

// Credential 携带解密后的明文 secret。Service 层从 git_credentials 行 + crypto.Encryptor.Decrypt
// 解出后构造 Credential，git 调用结束立即丢弃。
type Credential struct {
	Kind     CredentialKind
	Username string // PAT 时填，否则空
	Secret   []byte // PAT: token 字节；SSH: PEM-encoded private key；None: nil
}

// credentialEnv 把 cred 物化为临时文件 + env KEY=VALUE 切片，并返回 cleanup 闭包。
// tmpDir 必须存在且可写；调用方在 git 命令完成后必须 defer cleanup()。
//
// nil cred 等价于 Kind==CredentialNone：返回 nil env、nil cleanup。
func credentialEnv(tmpDir string, cred *Credential) (env []string, cleanup func(), err error) {
	if cred == nil || cred.Kind == CredentialNone {
		return nil, func() {}, nil
	}
	switch cred.Kind {
	case CredentialPAT:
		return patEnv(tmpDir, cred)
	case CredentialSSHKey:
		return sshKeyEnv(tmpDir, cred)
	default:
		return nil, func() {}, fmt.Errorf("unknown credential kind %q", cred.Kind)
	}
}

// patEnv 创建 askpass 脚本（Linux .sh / Windows .bat），并把 username 与 token
// 各写入一个 0600 临时文件，脚本按提示前缀（git 固定以 "Username for"/"Password for"
// 开头）路由：命中 Username 输出用户名文件，否则输出 token 文件。
//
// 用文件 + type/cat 输出内容而非 echo env，规避 token 含 & | < > " 等特殊字符时的
// 命令注入/认证失败，也避免 token 经 env 泄漏给所有子进程。git 会自行裁掉输出末尾
// 的 CR/LF。
func patEnv(tmpDir string, cred *Credential) ([]string, func(), error) {
	username := cred.Username
	if username == "" {
		// GitHub PAT 习惯 username='git'；GitLab/Gitea 类似
		username = "git"
	}
	suffix := ".sh"
	body := patScriptShBody()
	mode := os.FileMode(0o700)
	if runtime.GOOS == "windows" {
		suffix = ".bat"
		body = patScriptBatBody()
		mode = 0o755
	}
	f, err := os.CreateTemp(tmpDir, "askpass-*"+suffix)
	if err != nil {
		return nil, func() {}, fmt.Errorf("create askpass: %w", err)
	}
	if _, err := f.WriteString(body); err != nil {
		_ = f.Close()
		_ = os.Remove(f.Name())
		return nil, func() {}, fmt.Errorf("write askpass: %w", err)
	}
	_ = f.Close()
	if err := os.Chmod(f.Name(), mode); err != nil {
		_ = os.Remove(f.Name())
		return nil, func() {}, fmt.Errorf("chmod askpass: %w", err)
	}

	userFile, err := writeSecretFile(tmpDir, "askpass-user-*", []byte(username))
	if err != nil {
		_ = os.Remove(f.Name())
		return nil, func() {}, fmt.Errorf("write username file: %w", err)
	}
	tokenFile, err := writeSecretFile(tmpDir, "askpass-token-*", cred.Secret)
	if err != nil {
		_ = os.Remove(f.Name())
		_ = os.Remove(userFile)
		return nil, func() {}, fmt.Errorf("write token file: %w", err)
	}

	env := []string{
		"GIT_ASKPASS=" + f.Name(),
		"GIT_PAT_USERNAME_FILE=" + userFile,
		"GIT_PAT_TOKEN_FILE=" + tokenFile,
		// GIT_TERMINAL_PROMPT=0 防止 askpass 失效时 git 退回交互
		"GIT_TERMINAL_PROMPT=0",
	}
	cleanup := func() {
		_ = os.Remove(f.Name())
		_ = os.Remove(userFile)
		_ = os.Remove(tokenFile)
	}
	return env, cleanup, nil
}

// writeSecretFile 把 data 写入 tmpDir 下的 0600 临时文件，返回路径。
func writeSecretFile(tmpDir, pattern string, data []byte) (string, error) {
	f, err := os.CreateTemp(tmpDir, pattern)
	if err != nil {
		return "", err
	}
	if _, err := f.Write(data); err != nil {
		_ = f.Close()
		_ = os.Remove(f.Name())
		return "", err
	}
	_ = f.Close()
	if err := os.Chmod(f.Name(), 0o600); err != nil {
		_ = os.Remove(f.Name())
		return "", err
	}
	return f.Name(), nil
}

func patScriptShBody() string {
	return `#!/bin/sh
case "$1" in
  Username*) cat "$GIT_PAT_USERNAME_FILE" ;;
  *) cat "$GIT_PAT_TOKEN_FILE" ;;
esac
`
}

func patScriptBatBody() string {
	return "@echo off\r\n" +
		"echo %1 | findstr /i /c:\"Username for\" >NUL\r\n" +
		"if %ERRORLEVEL%==0 (type \"%GIT_PAT_USERNAME_FILE%\") else (type \"%GIT_PAT_TOKEN_FILE%\")\r\n"
}

// sshKeyEnv 写临时密钥 + 空 known_hosts，构造 GIT_SSH_COMMAND。
//
//	Linux: chmod 0600 已通过 os.WriteFile mode 0600 实现
//	Windows: 通过 icacls 限权（OpenSSH for Windows 检查文件 ACL）
func sshKeyEnv(tmpDir string, cred *Credential) ([]string, func(), error) {
	keyFile, err := os.CreateTemp(tmpDir, "ssh-key-*")
	if err != nil {
		return nil, func() {}, fmt.Errorf("create ssh key tmp: %w", err)
	}
	// 规整换行：\r\n 与单独 \r 统一为 \n，并确保末尾有且仅有一个 \n。
	// OpenSSH 新格式 / ED25519 密钥缺尾换行会报 invalid format。不改 cred.Secret 本身。
	key := bytes.ReplaceAll(cred.Secret, []byte("\r\n"), []byte("\n"))
	key = bytes.ReplaceAll(key, []byte("\r"), []byte("\n"))
	key = append(bytes.TrimRight(key, "\n"), '\n')
	if _, err := keyFile.Write(key); err != nil {
		_ = keyFile.Close()
		_ = os.Remove(keyFile.Name())
		return nil, func() {}, fmt.Errorf("write ssh key: %w", err)
	}
	_ = keyFile.Close()
	if err := os.Chmod(keyFile.Name(), 0o600); err != nil {
		_ = os.Remove(keyFile.Name())
		return nil, func() {}, fmt.Errorf("chmod ssh key: %w", err)
	}
	if runtime.GOOS == "windows" {
		if err := windowsRestrictACL(keyFile.Name()); err != nil {
			_ = os.Remove(keyFile.Name())
			return nil, func() {}, fmt.Errorf("icacls ssh key: %w", err)
		}
	}

	khFile, err := os.CreateTemp(tmpDir, "known-hosts-*")
	if err != nil {
		_ = os.Remove(keyFile.Name())
		return nil, func() {}, fmt.Errorf("create known_hosts tmp: %w", err)
	}
	_ = khFile.Close()

	// BatchMode=yes：带 passphrase 私钥或任何交互需求立即失败并给出明确 stderr，
	// 而非挂起。
	cmd := fmt.Sprintf(
		"ssh -i %q -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%q",
		keyFile.Name(), khFile.Name(),
	)
	env := []string{"GIT_SSH_COMMAND=" + cmd}
	cleanup := func() {
		_ = os.Remove(keyFile.Name())
		_ = os.Remove(khFile.Name())
	}
	return env, cleanup, nil
}

// windowsRestrictACL 通过 icacls 把文件 ACL 限制为当前用户独占读取。
// OpenSSH 否则会拒绝使用密钥（"Permissions for 'xxx' are too open"）。
func windowsRestrictACL(path string) error {
	if runtime.GOOS != "windows" {
		return nil
	}
	user := os.Getenv("USERNAME")
	if user == "" {
		return errors.New("USERNAME env empty on windows")
	}
	out, err := runWindowsTool("icacls", path, "/inheritance:r", "/grant:r", user+":(R)")
	if err != nil {
		return fmt.Errorf("icacls failed: %w (%s)", err, out)
	}
	return nil
}

// runWindowsTool 把 icacls 调用单独抽出便于测试 stub。
var runWindowsTool = func(name string, args ...string) (string, error) {
	return runExec(name, args...)
}

// 通过 filepath.Join 引用避免 import 被 lint 抹掉
var _ = filepath.Join

// runExec 执行外部小工具（icacls 等），返回 stdout+stderr 与错误。
// 不复用 DefaultRunner.exec 以避免对 Config 的循环依赖。
func runExec(name string, args ...string) (string, error) {
	out, err := osExec(name, args...)
	return string(out), err
}

// osExec 是 os/exec.Command 的薄包装，暴露成变量便于测试 stub。
var osExec = func(name string, args ...string) ([]byte, error) {
	return exec.Command(name, args...).CombinedOutput()
}
