package agent

import (
	"context"
	"strings"
	"unicode/utf8"

	"webby-builder/internal/client/laravel"
	gitpkg "webby-builder/internal/git"
	"webby-builder/internal/models"
)

const githubBotName = "Webby[bot]"
const githubBotEmail = "webby[bot]@users.noreply.github.com"

type pushDecision int

const (
	pushInitial pushDecision = iota
	pushRebase
	pushDiverged
)

func decidePush(remoteSHA, lastPushedSHA string) pushDecision {
	if remoteSHA == "" {
		return pushInitial
	}
	if remoteSHA == lastPushedSHA {
		return pushRebase
	}
	return pushDiverged
}

func commitMessageFromGoal(goal string) string {
	for _, line := range strings.Split(goal, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		if len(line) > 64 {
			line = line[:64]
		}
		return "Webby: " + line
	}
	return "Webby: update site"
}

// commitMessageFunc produces a commit subject from the staged diff stat. nil =>
// the goal-derived default is used.
type commitMessageFunc func(ctx context.Context, diffStat string) string

// sanitizeCommitMessage normalises an LLM-produced message to a single clean
// subject line (<=72 chars). Returns "" when nothing usable remains.
func sanitizeCommitMessage(raw string) string {
	line := strings.TrimSpace(raw)
	if i := strings.IndexAny(line, "\r\n"); i >= 0 {
		line = strings.TrimSpace(line[:i])
	}
	line = strings.Trim(line, "`\"' ")
	line = strings.TrimSpace(line)
	if line == "" {
		return ""
	}
	if utf8.RuneCountInString(line) > 72 {
		line = strings.TrimSpace(string([]rune(line)[:72]))
	}
	return line
}

type msgSender interface{ SendMessage(content string) }

// githubIgnorePatterns are excluded from every push so the linked repo contains
// only the real project source — never dependencies, build output,
// builder-internal artifacts, or secrets.
var githubIgnorePatterns = []string{
	"node_modules/",
	"dist/",
	".revisions/",
	"KNOWLEDGE.md",
	"memory.json",
	"design-intelligence.json",
	"template.json",
	"*.log",
	".DS_Store",
	".env",
	".env.*",
}

// runGithubPush mints a token, mirrors the workspace to the repo, and reports the
// new sha. Best-effort: failures post a chat message and return. The commit
// subject comes from msg (falling back to the goal-derived default). Returns ok
// + a short status summary (used by the pushToGithub tool result).
func runGithubPush(ctx context.Context, workspaceDir, sessionID, goal string, ghCap *models.GithubCapability, lc *laravel.Client, streamer msgSender, msg commitMessageFunc) (bool, string) {
	branch := ghCap.DefaultBranch
	if branch == "" {
		branch = "main"
	}

	tok, err := lc.MintGithubToken(sessionID)
	if err != nil || !tok.OK || tok.Token == "" {
		streamer.SendMessage("Couldn't reach GitHub to push your changes. Your build is saved; try again shortly.")
		return false, "Couldn't reach GitHub."
	}
	if !strings.HasPrefix(tok.CloneURL, "https://") {
		streamer.SendMessage("Couldn't push to GitHub: unexpected clone URL. Your build is saved.")
		return false, "Unexpected clone URL."
	}
	authURL := "https://x-access-token:" + tok.Token + "@" + tok.CloneURL[len("https://"):]

	repo := &gitpkg.Repo{Dir: workspaceDir, RemoteURL: authURL, Branch: branch,
		AuthorName: githubBotName, AuthorEmail: githubBotEmail}

	remoteSHA, err := gitpkg.RemoteBranchSHA(ctx, authURL, branch)
	if err != nil {
		streamer.SendMessage("Couldn't read your GitHub repository to push changes. Your build is saved.")
		return false, "Couldn't read the repository."
	}

	switch decidePush(remoteSHA, tok.LastPushedSHA) {
	case pushDiverged:
		streamer.SendMessage("Your GitHub repo's " + branch + " branch has changes made outside Webby, so I didn't push to avoid overwriting them. Reconcile the repo and I'll push next time.")
		return false, "The repo diverged (changed outside Webby); not pushed."
	case pushRebase:
		if err := repo.Ensure(ctx); err != nil {
			streamer.SendMessage("Couldn't prepare the repository to push. Your build is saved.")
			return false, "Couldn't prepare the repository."
		}
		if err := repo.BaseOnRemote(ctx); err != nil {
			streamer.SendMessage("Couldn't sync with your GitHub repo to push. Your build is saved.")
			return false, "Couldn't sync with the repository."
		}
	case pushInitial:
		if err := repo.Ensure(ctx); err != nil {
			streamer.SendMessage("Couldn't prepare the repository to push. Your build is saved.")
			return false, "Couldn't prepare the repository."
		}
	}

	if err := repo.EnsureGitignore(ctx, githubIgnorePatterns); err != nil {
		streamer.SendMessage("Couldn't prepare the repository to push. Your build is saved.")
		return false, "Couldn't prepare repository ignores."
	}

	hasChanges, err := repo.StageAll(ctx)
	if err != nil {
		streamer.SendMessage("Couldn't stage your changes for GitHub. Your build is saved.")
		return false, "Couldn't stage changes."
	}
	if !hasChanges {
		return false, "No changes to push."
	}

	// Plan-gated copyright mark: embed the attribution Laravel sent with the
	// token into the staged index.html blob only (the working tree stays
	// clean). Attribution is a nudge, not DRM — a failure never blocks the
	// user's push.
	if tok.Copyright != nil {
		_ = repo.StageAttribution(ctx, tok.Copyright.HTML, tok.Copyright.Comment)
	}

	commitMsg := commitMessageFromGoal(goal)
	if msg != nil {
		if stat, derr := repo.StagedDiffStat(ctx); derr == nil {
			if m := msg(ctx, stat); m != "" {
				commitMsg = m
			}
		}
	}

	if err := repo.Commit(ctx, commitMsg); err != nil {
		streamer.SendMessage("Couldn't commit your changes for GitHub. Your build is saved.")
		return false, "Couldn't commit changes."
	}
	if err := repo.Push(ctx); err != nil {
		streamer.SendMessage("Couldn't push to GitHub (the repo may have changed). Your build is saved; I'll retry next build.")
		return false, "Couldn't push (the repo may have changed)."
	}
	if head, err := repo.HeadSHA(ctx); err == nil {
		_ = lc.ReportGithubPush(sessionID, head)
	}
	streamer.SendMessage("Pushed your latest changes to GitHub (" + ghCap.Owner + "/" + ghCap.Name + ", " + branch + ").")
	return true, "Pushed to " + ghCap.Owner + "/" + ghCap.Name + " (" + branch + ")."
}

// SyncGithub auto-pushes a successful build when auto-push is enabled.
func SyncGithub(ctx context.Context, workspaceDir, sessionID, goal string, ghCap *models.GithubCapability, lc *laravel.Client, streamer msgSender, msg commitMessageFunc) {
	if ghCap == nil || !ghCap.Enabled || !ghCap.AutoPush {
		return
	}
	runGithubPush(ctx, workspaceDir, sessionID, goal, ghCap, lc, streamer, msg)
}

// PushGithubNow performs an explicit, user-requested push (ignores the auto-push
// setting). Returns ok + a status summary for the agent tool result.
func PushGithubNow(ctx context.Context, workspaceDir, sessionID, goal string, ghCap *models.GithubCapability, lc *laravel.Client, streamer msgSender, msg commitMessageFunc) (bool, string) {
	if ghCap == nil || !ghCap.Enabled {
		return false, "GitHub is not enabled for this project."
	}
	return runGithubPush(ctx, workspaceDir, sessionID, goal, ghCap, lc, streamer, msg)
}
