package workspace

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/url"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"chat-switchboard/crypto"
	"chat-switchboard/models"
	"chat-switchboard/store"
)

// GitOps provides git operations for workspaces via exec.
// Credentials are decrypted from the vault and injected via temporary
// files or environment variables for the duration of each operation.
type GitOps struct {
	fs          *FS
	stores      store.Stores
	keyResolver *crypto.KeyResolver
	indexer     *Indexer // may be nil
}

// NewGitOps creates a new git operations handler.
func NewGitOps(fs *FS, stores store.Stores, resolver *crypto.KeyResolver, indexer *Indexer) *GitOps {
	return &GitOps{
		fs:          fs,
		stores:      stores,
		keyResolver: resolver,
		indexer:     indexer,
	}
}

// ── Clone ────────────────────────────────────

// Clone clones a remote repository into the workspace.
func (g *GitOps) Clone(ctx context.Context, w *models.Workspace, remoteURL, branch, credID string) error {
	if err := validateRemoteURL(remoteURL); err != nil {
		return err
	}

	dir := g.fs.workspacePath(w)

	args := []string{"clone", "--single-branch"}
	if branch != "" {
		args = append(args, "--branch", branch)
	}
	args = append(args, remoteURL, ".")

	env, cleanup, err := g.credEnv(ctx, credID, remoteURL)
	if err != nil {
		return fmt.Errorf("git clone: credential setup: %w", err)
	}
	defer cleanup()

	if _, err := g.run(ctx, dir, env, args...); err != nil {
		return fmt.Errorf("git clone: %w", err)
	}

	// Update workspace with git metadata
	now := time.Now().UTC()
	patch := models.WorkspacePatch{
		GitRemoteURL: &remoteURL,
		GitCredentialID: &credID,
	}
	if branch != "" {
		patch.GitBranch = &branch
	} else {
		// Detect default branch
		if out, err := g.run(ctx, dir, nil, "rev-parse", "--abbrev-ref", "HEAD"); err == nil {
			b := strings.TrimSpace(out)
			patch.GitBranch = &b
		}
	}
	if err := g.stores.Workspaces.Update(ctx, w.ID, patch); err != nil {
		return fmt.Errorf("git clone: update workspace: %w", err)
	}
	g.updateSyncTime(ctx, w.ID, now)

	// Post-operation: reconcile + re-index
	g.postOpHook(ctx, w)

	return nil
}

// ── Pull ─────────────────────────────────────

func (g *GitOps) Pull(ctx context.Context, w *models.Workspace) error {
	dir := g.fs.workspacePath(w)
	env, cleanup, err := g.credEnvFromWorkspace(ctx, w)
	if err != nil {
		return fmt.Errorf("git pull: %w", err)
	}
	defer cleanup()

	if _, err := g.run(ctx, dir, env, "pull", "--ff-only"); err != nil {
		return fmt.Errorf("git pull: %w", err)
	}

	g.updateSyncTime(ctx, w.ID, time.Now().UTC())
	g.postOpHook(ctx, w)
	return nil
}

// ── Push ─────────────────────────────────────

func (g *GitOps) Push(ctx context.Context, w *models.Workspace) error {
	dir := g.fs.workspacePath(w)
	env, cleanup, err := g.credEnvFromWorkspace(ctx, w)
	if err != nil {
		return fmt.Errorf("git push: %w", err)
	}
	defer cleanup()

	if _, err := g.run(ctx, dir, env, "push"); err != nil {
		return fmt.Errorf("git push: %w", err)
	}

	g.updateSyncTime(ctx, w.ID, time.Now().UTC())
	return nil
}

// ── Status ───────────────────────────────────

func (g *GitOps) Status(ctx context.Context, w *models.Workspace) (*models.GitStatus, error) {
	dir := g.fs.workspacePath(w)

	// Branch name
	branchOut, err := g.run(ctx, dir, nil, "rev-parse", "--abbrev-ref", "HEAD")
	if err != nil {
		return nil, fmt.Errorf("git status: %w", err)
	}

	// Porcelain status
	out, err := g.run(ctx, dir, nil, "status", "--porcelain=v1", "-b")
	if err != nil {
		return nil, fmt.Errorf("git status: %w", err)
	}

	status := &models.GitStatus{
		Branch: strings.TrimSpace(branchOut),
	}

	lines := strings.Split(strings.TrimSpace(out), "\n")
	for _, line := range lines {
		if len(line) == 0 {
			continue
		}
		// Branch line: ## main...origin/main [ahead 1, behind 2]
		if strings.HasPrefix(line, "## ") {
			if idx := strings.Index(line, "["); idx > 0 {
				bracket := line[idx:]
				if n := parseAheadBehind(bracket, "ahead"); n > 0 {
					status.Ahead = n
				}
				if n := parseAheadBehind(bracket, "behind"); n > 0 {
					status.Behind = n
				}
			}
			continue
		}
		if len(line) < 4 {
			continue
		}
		x := line[0]   // index status
		y := line[1]   // working tree status
		path := strings.TrimSpace(line[3:])

		if x == '?' && y == '?' {
			status.Untracked = append(status.Untracked, path)
		} else if x != ' ' && x != '?' {
			status.Staged = append(status.Staged, models.GitFileStatus{Path: path, Status: string(x)})
		}
		if y != ' ' && y != '?' {
			status.Modified = append(status.Modified, models.GitFileStatus{Path: path, Status: string(y)})
		}
	}

	status.Clean = len(status.Staged) == 0 && len(status.Modified) == 0 && len(status.Untracked) == 0
	return status, nil
}

// ── Diff ─────────────────────────────────────

func (g *GitOps) Diff(ctx context.Context, w *models.Workspace, path string) (string, error) {
	dir := g.fs.workspacePath(w)
	args := []string{"diff"}
	if path != "" {
		args = append(args, "--", path)
	}
	out, err := g.run(ctx, dir, nil, args...)
	if err != nil {
		return "", fmt.Errorf("git diff: %w", err)
	}
	return out, nil
}

// ── Commit ───────────────────────────────────

func (g *GitOps) Commit(ctx context.Context, w *models.Workspace, message string, paths []string) error {
	dir := g.fs.workspacePath(w)

	// Stage files
	if len(paths) == 0 {
		if _, err := g.run(ctx, dir, nil, "add", "-A"); err != nil {
			return fmt.Errorf("git add: %w", err)
		}
	} else {
		args := append([]string{"add", "--"}, paths...)
		if _, err := g.run(ctx, dir, nil, args...); err != nil {
			return fmt.Errorf("git add: %w", err)
		}
	}

	// Commit
	if _, err := g.run(ctx, dir, nil, "commit", "-m", message); err != nil {
		return fmt.Errorf("git commit: %w", err)
	}

	return nil
}

// ── Log ──────────────────────────────────────

func (g *GitOps) Log(ctx context.Context, w *models.Workspace, n int) ([]models.GitLogEntry, error) {
	dir := g.fs.workspacePath(w)
	if n <= 0 {
		n = 20
	}

	// JSON-like format for reliable parsing
	format := `{"hash":"%H","short_hash":"%h","author":"%an","date":"%aI","message":"%s"}`
	out, err := g.run(ctx, dir, nil, "log", fmt.Sprintf("-n%d", n), fmt.Sprintf("--format=%s", format))
	if err != nil {
		return nil, fmt.Errorf("git log: %w", err)
	}

	var entries []models.GitLogEntry
	scanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(out)))
	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			continue
		}
		var entry models.GitLogEntry
		if err := json.Unmarshal([]byte(line), &entry); err != nil {
			continue // skip malformed
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

// ── Branch ───────────────────────────────────

func (g *GitOps) BranchList(ctx context.Context, w *models.Workspace) ([]string, string, error) {
	dir := g.fs.workspacePath(w)
	out, err := g.run(ctx, dir, nil, "branch", "-a", "--format=%(refname:short)%(if)%(HEAD)%(then) *%(end)")
	if err != nil {
		return nil, "", fmt.Errorf("git branch: %w", err)
	}

	var branches []string
	var current string
	for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
		if line == "" {
			continue
		}
		if strings.HasSuffix(line, " *") {
			name := strings.TrimSuffix(line, " *")
			current = name
			branches = append(branches, name)
		} else {
			branches = append(branches, strings.TrimSpace(line))
		}
	}
	return branches, current, nil
}

// Checkout switches to a branch.
func (g *GitOps) Checkout(ctx context.Context, w *models.Workspace, branch string) error {
	dir := g.fs.workspacePath(w)
	if _, err := g.run(ctx, dir, nil, "checkout", branch); err != nil {
		return fmt.Errorf("git checkout: %w", err)
	}

	// Update workspace branch
	patch := models.WorkspacePatch{GitBranch: &branch}
	if err := g.stores.Workspaces.Update(ctx, w.ID, patch); err != nil {
		log.Printf("git: failed to update workspace branch: %v", err)
	}

	g.postOpHook(ctx, w)
	return nil
}

// ── Internals ────────────────────────────────

// run executes a git command in the given directory with optional env vars.
func (g *GitOps) run(ctx context.Context, dir string, env []string, args ...string) (string, error) {
	cmd := exec.CommandContext(ctx, "git", args...)
	cmd.Dir = dir

	// Base environment + any credential env vars
	cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
	if len(env) > 0 {
		cmd.Env = append(cmd.Env, env...)
	}

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		// Include stderr for diagnostics but sanitize credentials
		errMsg := sanitizeOutput(stderr.String())
		return "", fmt.Errorf("%s: %s", err, errMsg)
	}
	return stdout.String(), nil
}

// credEnv sets up credential environment for a git operation.
// Returns env vars, a cleanup function, and any error.
func (g *GitOps) credEnv(ctx context.Context, credID, remoteURL string) ([]string, func(), error) {
	noop := func() {}
	if credID == "" {
		return nil, noop, nil
	}

	cred, err := g.stores.GitCredentials.GetByID(ctx, credID)
	if err != nil {
		return nil, noop, fmt.Errorf("credential not found: %w", err)
	}

	// Decrypt credential data
	plaintext, err := g.keyResolver.Decrypt(cred.EncryptedData, cred.Nonce, "global", cred.UserID)
	if err != nil {
		return nil, noop, fmt.Errorf("credential decryption failed: %w", err)
	}

	var data map[string]string
	if err := json.Unmarshal([]byte(plaintext), &data); err != nil {
		return nil, noop, fmt.Errorf("credential data invalid: %w", err)
	}

	switch cred.AuthType {
	case "https_pat":
		return g.setupHTTPSPAT(data, remoteURL)
	case "https_basic":
		return g.setupHTTPSBasic(data, remoteURL)
	case "ssh_key":
		return g.setupSSHKey(data)
	default:
		return nil, noop, fmt.Errorf("unsupported auth type: %s", cred.AuthType)
	}
}

func (g *GitOps) credEnvFromWorkspace(ctx context.Context, w *models.Workspace) ([]string, func(), error) {
	credID := ""
	if w.GitCredentialID != nil {
		credID = *w.GitCredentialID
	}
	remoteURL := ""
	if w.GitRemoteURL != nil {
		remoteURL = *w.GitRemoteURL
	}
	return g.credEnv(ctx, credID, remoteURL)
}

// setupHTTPSPAT creates a GIT_ASKPASS script that echoes the PAT.
func (g *GitOps) setupHTTPSPAT(data map[string]string, remoteURL string) ([]string, func(), error) {
	token := data["token"]
	if token == "" {
		return nil, func() {}, fmt.Errorf("PAT token is empty")
	}

	// Create temp askpass script
	f, err := os.CreateTemp("", "git-askpass-*.sh")
	if err != nil {
		return nil, func() {}, err
	}

	script := fmt.Sprintf("#!/bin/sh\necho '%s'\n", strings.ReplaceAll(token, "'", "'\\''"))
	f.WriteString(script)
	f.Close()
	os.Chmod(f.Name(), 0700)

	cleanup := func() {
		os.Remove(f.Name())
	}

	env := []string{
		"GIT_ASKPASS=" + f.Name(),
	}
	return env, cleanup, nil
}

// setupHTTPSBasic creates a temporary .git-credentials file.
func (g *GitOps) setupHTTPSBasic(data map[string]string, remoteURL string) ([]string, func(), error) {
	username := data["username"]
	password := data["password"]
	if username == "" || password == "" {
		return nil, func() {}, fmt.Errorf("basic auth credentials incomplete")
	}

	// Parse remote URL to extract host
	parsed, err := url.Parse(remoteURL)
	if err != nil {
		return nil, func() {}, fmt.Errorf("invalid remote URL: %w", err)
	}

	credURL := fmt.Sprintf("%s://%s:%s@%s", parsed.Scheme,
		url.PathEscape(username), url.PathEscape(password), parsed.Host)

	f, err := os.CreateTemp("", "git-creds-*.txt")
	if err != nil {
		return nil, func() {}, err
	}
	f.WriteString(credURL + "\n")
	f.Close()
	os.Chmod(f.Name(), 0600)

	cleanup := func() {
		os.Remove(f.Name())
	}

	env := []string{
		fmt.Sprintf("GIT_CONFIG_COUNT=2"),
		fmt.Sprintf("GIT_CONFIG_KEY_0=credential.helper"),
		fmt.Sprintf("GIT_CONFIG_VALUE_0=store --file=%s", f.Name()),
		fmt.Sprintf("GIT_CONFIG_KEY_1=credential.helper"),
		fmt.Sprintf("GIT_CONFIG_VALUE_1="),
	}
	return env, cleanup, nil
}

// setupSSHKey creates a temporary SSH key file and GIT_SSH_COMMAND.
func (g *GitOps) setupSSHKey(data map[string]string) ([]string, func(), error) {
	privateKey := data["private_key"]
	if privateKey == "" {
		return nil, func() {}, fmt.Errorf("SSH private key is empty")
	}

	f, err := os.CreateTemp("", "git-ssh-key-*")
	if err != nil {
		return nil, func() {}, err
	}
	f.WriteString(privateKey)
	if !strings.HasSuffix(privateKey, "\n") {
		f.WriteString("\n")
	}
	f.Close()
	os.Chmod(f.Name(), 0600)

	cleanup := func() {
		os.Remove(f.Name())
	}

	sshCmd := fmt.Sprintf("ssh -i %s -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=yes", f.Name())

	env := []string{
		"GIT_SSH_COMMAND=" + sshCmd,
	}
	return env, cleanup, nil
}

// validateRemoteURL rejects file:// and local path URLs.
func validateRemoteURL(u string) error {
	if u == "" {
		return fmt.Errorf("remote URL is required")
	}
	lower := strings.ToLower(u)
	if strings.HasPrefix(lower, "file://") || strings.HasPrefix(lower, "/") || strings.HasPrefix(lower, ".") {
		return fmt.Errorf("local filesystem URLs are not allowed")
	}
	return nil
}

// sanitizeOutput removes potential credential leaks from error messages.
func sanitizeOutput(s string) string {
	// Strip lines containing tokens/passwords
	var lines []string
	for _, line := range strings.Split(s, "\n") {
		lower := strings.ToLower(line)
		if strings.Contains(lower, "password") || strings.Contains(lower, "token") ||
			strings.Contains(lower, "credential") {
			continue
		}
		lines = append(lines, line)
	}
	result := strings.Join(lines, "\n")
	if len(result) > 500 {
		result = result[:500] + "..."
	}
	return strings.TrimSpace(result)
}

func parseAheadBehind(bracket, key string) int {
	idx := strings.Index(bracket, key)
	if idx < 0 {
		return 0
	}
	rest := bracket[idx+len(key):]
	rest = strings.TrimLeft(rest, " ")
	num := ""
	for _, c := range rest {
		if c >= '0' && c <= '9' {
			num += string(c)
		} else {
			break
		}
	}
	n, _ := strconv.Atoi(num)
	return n
}

// updateSyncTime sets git_last_sync on the workspace.
func (g *GitOps) updateSyncTime(ctx context.Context, wsID string, t time.Time) {
	if err := g.stores.Workspaces.SetGitLastSync(ctx, wsID, t); err != nil {
		log.Printf("git: failed to update sync time for %s: %v", wsID, err)
	}
}

// postOpHook triggers workspace reconcile and re-indexing after git operations
// that change the working tree (clone, pull, checkout).
func (g *GitOps) postOpHook(ctx context.Context, w *models.Workspace) {
	// Reconcile: sync filesystem → database file index
	added, removed, updated, err := g.fs.Reconcile(ctx, w)
	if err != nil {
		log.Printf("git: post-op reconcile failed for %s: %v", w.ID, err)
		return
	}
	log.Printf("git: post-op reconcile %s: +%d -%d ~%d", w.ID, added, removed, updated)

	// Re-index if indexer is configured and there are changes
	if g.indexer != nil && g.indexer.IsEnabled(ctx) && (added+updated) > 0 {
		files, err := g.stores.Workspaces.ListFiles(ctx, w.ID, "", true)
		if err != nil {
			log.Printf("git: post-op index list failed: %v", err)
			return
		}

		// Resolve userID/teamID from workspace owner
		var userID string
		var teamID *string
		switch w.OwnerType {
		case "user":
			userID = w.OwnerID
		case "team":
			teamID = &w.OwnerID
		default:
			userID = w.OwnerID
		}

		g.indexer.IndexBatch(w, files, userID, teamID)
	}
}

// workspacePath returns the filesystem path for git operations on a workspace.
// This is the files directory where the working tree lives.
func (fs *FS) workspacePath(w *models.Workspace) string {
	return fs.filesDir(w)
}

// LoadWorkspace fetches a workspace by ID and validates it has a git remote.
// Used by git tools to resolve workspace + check git configuration.
func (g *GitOps) LoadWorkspace(ctx context.Context, workspaceID string) (*models.Workspace, error) {
	w, err := g.stores.Workspaces.GetByID(ctx, workspaceID)
	if err != nil {
		return nil, fmt.Errorf("workspace not found: %w", err)
	}
	if w.GitRemoteURL == nil || *w.GitRemoteURL == "" {
		return nil, fmt.Errorf("workspace has no git remote configured")
	}
	return w, nil
}
