// cmd_watch.go implements the 'qa watch' command for monitoring GitHub Actions.
//
// Usage:
//   core qa watch              # Watch current repo's latest push
//   core qa watch --repo X     # Watch specific repo
//   core qa watch --commit SHA # Watch specific commit
//   core qa watch --timeout 5m # Custom timeout (default: 10m)

package qa

import (
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"strings"
	"time"

	"github.com/host-uk/core/pkg/cli"
	"github.com/host-uk/core/pkg/i18n"
	"github.com/host-uk/core/pkg/log"
)

// Watch command flags
var (
	watchRepo    string
	watchCommit  string
	watchTimeout time.Duration
)

// WorkflowRun represents a GitHub Actions workflow run
type WorkflowRun struct {
	ID           int64     `json:"databaseId"`
	Name         string    `json:"name"`
	DisplayTitle string    `json:"displayTitle"`
	Status       string    `json:"status"`
	Conclusion   string    `json:"conclusion"`
	HeadSha      string    `json:"headSha"`
	URL          string    `json:"url"`
	CreatedAt    time.Time `json:"createdAt"`
	UpdatedAt    time.Time `json:"updatedAt"`
}

// WorkflowJob represents a job within a workflow run
type WorkflowJob struct {
	ID         int64  `json:"databaseId"`
	Name       string `json:"name"`
	Status     string `json:"status"`
	Conclusion string `json:"conclusion"`
	URL        string `json:"url"`
}

// JobStep represents a step within a job
type JobStep struct {
	Name       string `json:"name"`
	Status     string `json:"status"`
	Conclusion string `json:"conclusion"`
	Number     int    `json:"number"`
}

// addWatchCommand adds the 'watch' subcommand to the qa command.
func addWatchCommand(parent *cli.Command) {
	watchCmd := &cli.Command{
		Use:   "watch",
		Short: i18n.T("cmd.qa.watch.short"),
		Long:  i18n.T("cmd.qa.watch.long"),
		RunE: func(cmd *cli.Command, args []string) error {
			return runWatch()
		},
	}

	watchCmd.Flags().StringVarP(&watchRepo, "repo", "r", "", i18n.T("cmd.qa.watch.flag.repo"))
	watchCmd.Flags().StringVarP(&watchCommit, "commit", "c", "", i18n.T("cmd.qa.watch.flag.commit"))
	watchCmd.Flags().DurationVarP(&watchTimeout, "timeout", "t", 10*time.Minute, i18n.T("cmd.qa.watch.flag.timeout"))

	parent.AddCommand(watchCmd)
}

func runWatch() error {
	// Check gh is available
	if _, err := exec.LookPath("gh"); err != nil {
		return log.E("qa.watch", i18n.T("error.gh_not_found"), nil)
	}

	// Determine repo
	repoFullName, err := resolveRepo(watchRepo)
	if err != nil {
		return err
	}

	// Determine commit
	commitSha, err := resolveCommit(watchCommit)
	if err != nil {
		return err
	}

	cli.Print("%s %s\n", dimStyle.Render(i18n.Label("repo")), repoFullName)
	// Safe prefix for display - handle short SHAs gracefully
	shaPrefix := commitSha
	if len(commitSha) > 8 {
		shaPrefix = commitSha[:8]
	}
	cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.qa.watch.commit")), shaPrefix)
	cli.Blank()

	// Create context with timeout for all gh commands
	ctx, cancel := context.WithTimeout(context.Background(), watchTimeout)
	defer cancel()

	// Poll for workflow runs
	pollInterval := 3 * time.Second
	var lastStatus string

	for {
		// Check if context deadline exceeded
		if ctx.Err() != nil {
			cli.Blank()
			return log.E("qa.watch", i18n.T("cmd.qa.watch.timeout", map[string]interface{}{"Duration": watchTimeout}), nil)
		}

		runs, err := fetchWorkflowRunsForCommit(ctx, repoFullName, commitSha)
		if err != nil {
			return log.Wrap(err, "qa.watch", "failed to fetch workflow runs")
		}

		if len(runs) == 0 {
			// No workflows triggered yet, keep waiting
			cli.Print("\033[2K\r%s", dimStyle.Render(i18n.T("cmd.qa.watch.waiting_for_workflows")))
			time.Sleep(pollInterval)
			continue
		}

		// Check status of all runs
		allComplete := true
		var pending, success, failed int
		for _, run := range runs {
			switch run.Status {
			case "completed":
				if run.Conclusion == "success" {
					success++
				} else {
					// Count all non-success conclusions as failed
					// (failure, cancelled, timed_out, action_required, stale, etc.)
					failed++
				}
			default:
				allComplete = false
				pending++
			}
		}

		// Build status line
		status := fmt.Sprintf("%d workflow(s): ", len(runs))
		if pending > 0 {
			status += warningStyle.Render(fmt.Sprintf("%d running", pending))
			if success > 0 || failed > 0 {
				status += ", "
			}
		}
		if success > 0 {
			status += successStyle.Render(fmt.Sprintf("%d passed", success))
			if failed > 0 {
				status += ", "
			}
		}
		if failed > 0 {
			status += errorStyle.Render(fmt.Sprintf("%d failed", failed))
		}

		// Only print if status changed
		if status != lastStatus {
			cli.Print("\033[2K\r%s", status)
			lastStatus = status
		}

		if allComplete {
			cli.Blank()
			cli.Blank()
			return printResults(ctx, repoFullName, runs)
		}

		time.Sleep(pollInterval)
	}
}

// resolveRepo determines the repo to watch
func resolveRepo(specified string) (string, error) {
	if specified != "" {
		// If it contains /, assume it's already full name
		if strings.Contains(specified, "/") {
			return specified, nil
		}
		// Try to get org from current directory
		org := detectOrgFromGit()
		if org != "" {
			return org + "/" + specified, nil
		}
		return "", log.E("qa.watch", i18n.T("cmd.qa.watch.error.repo_format"), nil)
	}

	// Detect from current directory
	return detectRepoFromGit()
}

// resolveCommit determines the commit to watch
func resolveCommit(specified string) (string, error) {
	if specified != "" {
		return specified, nil
	}

	// Get HEAD commit
	cmd := exec.Command("git", "rev-parse", "HEAD")
	output, err := cmd.Output()
	if err != nil {
		return "", log.Wrap(err, "qa.watch", "failed to get HEAD commit")
	}

	return strings.TrimSpace(string(output)), nil
}

// detectRepoFromGit detects the repo from git remote
func detectRepoFromGit() (string, error) {
	cmd := exec.Command("git", "remote", "get-url", "origin")
	output, err := cmd.Output()
	if err != nil {
		return "", log.E("qa.watch", i18n.T("cmd.qa.watch.error.not_git_repo"), nil)
	}

	url := strings.TrimSpace(string(output))
	return parseGitHubRepo(url)
}

// detectOrgFromGit tries to detect the org from git remote
func detectOrgFromGit() string {
	repo, err := detectRepoFromGit()
	if err != nil {
		return ""
	}
	parts := strings.Split(repo, "/")
	if len(parts) >= 1 {
		return parts[0]
	}
	return ""
}

// parseGitHubRepo extracts org/repo from a git URL
func parseGitHubRepo(url string) (string, error) {
	// Handle SSH URLs: git@github.com:org/repo.git
	if strings.HasPrefix(url, "git@github.com:") {
		path := strings.TrimPrefix(url, "git@github.com:")
		path = strings.TrimSuffix(path, ".git")
		return path, nil
	}

	// Handle HTTPS URLs: https://github.com/org/repo.git
	if strings.Contains(url, "github.com/") {
		parts := strings.Split(url, "github.com/")
		if len(parts) >= 2 {
			path := strings.TrimSuffix(parts[1], ".git")
			return path, nil
		}
	}

	return "", fmt.Errorf("could not parse GitHub repo from URL: %s", url)
}

// fetchWorkflowRunsForCommit fetches workflow runs for a specific commit
func fetchWorkflowRunsForCommit(ctx context.Context, repoFullName, commitSha string) ([]WorkflowRun, error) {
	args := []string{
		"run", "list",
		"--repo", repoFullName,
		"--commit", commitSha,
		"--json", "databaseId,name,displayTitle,status,conclusion,headSha,url,createdAt,updatedAt",
	}

	cmd := exec.CommandContext(ctx, "gh", args...)
	output, err := cmd.Output()
	if err != nil {
		// Check if context was cancelled/deadline exceeded
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		if exitErr, ok := err.(*exec.ExitError); ok {
			return nil, cli.Err("%s", strings.TrimSpace(string(exitErr.Stderr)))
		}
		return nil, err
	}

	var runs []WorkflowRun
	if err := json.Unmarshal(output, &runs); err != nil {
		return nil, err
	}

	return runs, nil
}

// printResults prints the final results with actionable information
func printResults(ctx context.Context, repoFullName string, runs []WorkflowRun) error {
	var failures []WorkflowRun
	var successes []WorkflowRun

	for _, run := range runs {
		if run.Conclusion == "success" {
			successes = append(successes, run)
		} else {
			// Treat all non-success as failures (failure, cancelled, timed_out, etc.)
			failures = append(failures, run)
		}
	}

	// Print successes briefly
	for _, run := range successes {
		cli.Print("%s %s\n", successStyle.Render(cli.Glyph(":check:")), run.Name)
	}

	// Print failures with details
	for _, run := range failures {
		cli.Print("%s %s\n", errorStyle.Render(cli.Glyph(":cross:")), run.Name)

		// Fetch failed job details
		failedJob, failedStep, errorLine := fetchFailureDetails(ctx, repoFullName, run.ID)
		if failedJob != "" {
			cli.Print("  %s Job: %s", dimStyle.Render("->"), failedJob)
			if failedStep != "" {
				cli.Print(" (step: %s)", failedStep)
			}
			cli.Blank()
		}
		if errorLine != "" {
			cli.Print("  %s Error: %s\n", dimStyle.Render("->"), errorLine)
		}
		cli.Print("  %s %s\n", dimStyle.Render("->"), run.URL)
	}

	// Exit with error if any failures
	if len(failures) > 0 {
		cli.Blank()
		return cli.Err("%s", i18n.T("cmd.qa.watch.workflows_failed", map[string]interface{}{"Count": len(failures)}))
	}

	cli.Blank()
	cli.Print("%s\n", successStyle.Render(i18n.T("cmd.qa.watch.all_passed")))
	return nil
}

// fetchFailureDetails fetches details about why a workflow failed
func fetchFailureDetails(ctx context.Context, repoFullName string, runID int64) (jobName, stepName, errorLine string) {
	// Fetch jobs for this run
	args := []string{
		"run", "view", fmt.Sprintf("%d", runID),
		"--repo", repoFullName,
		"--json", "jobs",
	}

	cmd := exec.CommandContext(ctx, "gh", args...)
	output, err := cmd.Output()
	if err != nil {
		return "", "", ""
	}

	var result struct {
		Jobs []struct {
			Name       string `json:"name"`
			Conclusion string `json:"conclusion"`
			Steps      []struct {
				Name       string `json:"name"`
				Conclusion string `json:"conclusion"`
				Number     int    `json:"number"`
			} `json:"steps"`
		} `json:"jobs"`
	}

	if err := json.Unmarshal(output, &result); err != nil {
		return "", "", ""
	}

	// Find the failed job and step
	for _, job := range result.Jobs {
		if job.Conclusion == "failure" {
			jobName = job.Name
			for _, step := range job.Steps {
				if step.Conclusion == "failure" {
					stepName = fmt.Sprintf("%d: %s", step.Number, step.Name)
					break
				}
			}
			break
		}
	}

	// Try to get the error line from logs (if available)
	errorLine = fetchErrorFromLogs(ctx, repoFullName, runID)

	return jobName, stepName, errorLine
}

// fetchErrorFromLogs attempts to extract the first error line from workflow logs
func fetchErrorFromLogs(ctx context.Context, repoFullName string, runID int64) string {
	// Use gh run view --log-failed to get failed step logs
	args := []string{
		"run", "view", fmt.Sprintf("%d", runID),
		"--repo", repoFullName,
		"--log-failed",
	}

	cmd := exec.CommandContext(ctx, "gh", args...)
	output, err := cmd.Output()
	if err != nil {
		return ""
	}

	// Parse output to find the first meaningful error line
	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		// Skip common metadata/progress lines
		lower := strings.ToLower(line)
		if strings.HasPrefix(lower, "##[") { // GitHub Actions command markers
			continue
		}
		if strings.HasPrefix(line, "Run ") || strings.HasPrefix(line, "Running ") {
			continue
		}

		// Look for error indicators
		if strings.Contains(lower, "error") ||
			strings.Contains(lower, "failed") ||
			strings.Contains(lower, "fatal") ||
			strings.Contains(lower, "panic") ||
			strings.Contains(line, ": ") { // Likely a file:line or key: value format
			// Truncate long lines
			if len(line) > 120 {
				line = line[:117] + "..."
			}
			return line
		}
	}

	return ""
}
