package php

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"sync"

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

// QARunner orchestrates PHP QA checks using pkg/process.
type QARunner struct {
	dir     string
	fix     bool
	service *process.Service
	core    *framework.Core

	// Output tracking
	outputMu     sync.Mutex
	checkOutputs map[string][]string
}

// NewQARunner creates a QA runner for the given directory.
func NewQARunner(dir string, fix bool) (*QARunner, error) {
	// Create a Core with process service for the QA session
	core, err := framework.New(
		framework.WithName("process", process.NewService(process.Options{})),
	)
	if err != nil {
		return nil, cli.WrapVerb(err, "create", "process service")
	}

	svc, err := framework.ServiceFor[*process.Service](core, "process")
	if err != nil {
		return nil, cli.WrapVerb(err, "get", "process service")
	}

	runner := &QARunner{
		dir:          dir,
		fix:          fix,
		service:      svc,
		core:         core,
		checkOutputs: make(map[string][]string),
	}

	return runner, nil
}

// BuildSpecs creates RunSpecs for the given QA checks.
func (r *QARunner) BuildSpecs(checks []string) []process.RunSpec {
	specs := make([]process.RunSpec, 0, len(checks))

	for _, check := range checks {
		spec := r.buildSpec(check)
		if spec != nil {
			specs = append(specs, *spec)
		}
	}

	return specs
}

// buildSpec creates a RunSpec for a single check.
func (r *QARunner) buildSpec(check string) *process.RunSpec {
	switch check {
	case "audit":
		return &process.RunSpec{
			Name:    "audit",
			Command: "composer",
			Args:    []string{"audit", "--format=summary"},
			Dir:     r.dir,
		}

	case "fmt":
		formatter, found := DetectFormatter(r.dir)
		if !found {
			return nil
		}
		if formatter == FormatterPint {
			vendorBin := filepath.Join(r.dir, "vendor", "bin", "pint")
			cmd := "pint"
			if _, err := os.Stat(vendorBin); err == nil {
				cmd = vendorBin
			}
			args := []string{}
			if !r.fix {
				args = append(args, "--test")
			}
			return &process.RunSpec{
				Name:    "fmt",
				Command: cmd,
				Args:    args,
				Dir:     r.dir,
				After:   []string{"audit"},
			}
		}
		return nil

	case "stan":
		_, found := DetectAnalyser(r.dir)
		if !found {
			return nil
		}
		vendorBin := filepath.Join(r.dir, "vendor", "bin", "phpstan")
		cmd := "phpstan"
		if _, err := os.Stat(vendorBin); err == nil {
			cmd = vendorBin
		}
		return &process.RunSpec{
			Name:    "stan",
			Command: cmd,
			Args:    []string{"analyse", "--no-progress"},
			Dir:     r.dir,
			After:   []string{"fmt"},
		}

	case "psalm":
		_, found := DetectPsalm(r.dir)
		if !found {
			return nil
		}
		vendorBin := filepath.Join(r.dir, "vendor", "bin", "psalm")
		cmd := "psalm"
		if _, err := os.Stat(vendorBin); err == nil {
			cmd = vendorBin
		}
		args := []string{"--no-progress"}
		if r.fix {
			args = append(args, "--alter", "--issues=all")
		}
		return &process.RunSpec{
			Name:    "psalm",
			Command: cmd,
			Args:    args,
			Dir:     r.dir,
			After:   []string{"stan"},
		}

	case "test":
		// Check for Pest first, fall back to PHPUnit
		pestBin := filepath.Join(r.dir, "vendor", "bin", "pest")
		phpunitBin := filepath.Join(r.dir, "vendor", "bin", "phpunit")

		cmd := "pest"
		if _, err := os.Stat(pestBin); err == nil {
			cmd = pestBin
		} else if _, err := os.Stat(phpunitBin); err == nil {
			cmd = phpunitBin
		} else {
			return nil
		}

		// Tests depend on stan (or psalm if available)
		after := []string{"stan"}
		if _, found := DetectPsalm(r.dir); found {
			after = []string{"psalm"}
		}

		return &process.RunSpec{
			Name:    "test",
			Command: cmd,
			Args:    []string{},
			Dir:     r.dir,
			After:   after,
		}

	case "rector":
		if !DetectRector(r.dir) {
			return nil
		}
		vendorBin := filepath.Join(r.dir, "vendor", "bin", "rector")
		cmd := "rector"
		if _, err := os.Stat(vendorBin); err == nil {
			cmd = vendorBin
		}
		args := []string{"process"}
		if !r.fix {
			args = append(args, "--dry-run")
		}
		return &process.RunSpec{
			Name:         "rector",
			Command:      cmd,
			Args:         args,
			Dir:          r.dir,
			After:        []string{"test"},
			AllowFailure: true, // Dry-run returns non-zero if changes would be made
		}

	case "infection":
		if !DetectInfection(r.dir) {
			return nil
		}
		vendorBin := filepath.Join(r.dir, "vendor", "bin", "infection")
		cmd := "infection"
		if _, err := os.Stat(vendorBin); err == nil {
			cmd = vendorBin
		}
		return &process.RunSpec{
			Name:         "infection",
			Command:      cmd,
			Args:         []string{"--min-msi=50", "--min-covered-msi=70", "--threads=4"},
			Dir:          r.dir,
			After:        []string{"test"},
			AllowFailure: true,
		}
	}

	return nil
}

// Run executes all QA checks and returns the results.
func (r *QARunner) Run(ctx context.Context, stages []QAStage) (*QARunResult, error) {
	// Collect all checks from all stages
	var allChecks []string
	for _, stage := range stages {
		checks := GetQAChecks(r.dir, stage)
		allChecks = append(allChecks, checks...)
	}

	if len(allChecks) == 0 {
		return &QARunResult{Passed: true}, nil
	}

	// Build specs
	specs := r.BuildSpecs(allChecks)
	if len(specs) == 0 {
		return &QARunResult{Passed: true}, nil
	}

	// Register output handler
	r.core.RegisterAction(func(c *framework.Core, msg framework.Message) error {
		switch m := msg.(type) {
		case process.ActionProcessOutput:
			r.outputMu.Lock()
			// Extract check name from process ID mapping
			for _, spec := range specs {
				if strings.Contains(m.ID, spec.Name) || m.ID != "" {
					// Store output for later display if needed
					r.checkOutputs[spec.Name] = append(r.checkOutputs[spec.Name], m.Line)
					break
				}
			}
			r.outputMu.Unlock()
		}
		return nil
	})

	// Create runner and execute
	runner := process.NewRunner(r.service)
	result, err := runner.RunAll(ctx, specs)
	if err != nil {
		return nil, err
	}

	// Convert to QA result
	qaResult := &QARunResult{
		Passed:   result.Success(),
		Duration: result.Duration.String(),
		Results:  make([]QACheckRunResult, 0, len(result.Results)),
	}

	for _, res := range result.Results {
		qaResult.Results = append(qaResult.Results, QACheckRunResult{
			Name:     res.Name,
			Passed:   res.Passed(),
			Skipped:  res.Skipped,
			ExitCode: res.ExitCode,
			Duration: res.Duration.String(),
			Output:   res.Output,
		})
		if res.Passed() {
			qaResult.PassedCount++
		} else if res.Skipped {
			qaResult.SkippedCount++
		} else {
			qaResult.FailedCount++
		}
	}

	return qaResult, nil
}

// GetCheckOutput returns captured output for a check.
func (r *QARunner) GetCheckOutput(check string) []string {
	r.outputMu.Lock()
	defer r.outputMu.Unlock()
	return r.checkOutputs[check]
}

// QARunResult holds the results of running QA checks.
type QARunResult struct {
	Passed       bool               `json:"passed"`
	Duration     string             `json:"duration"`
	Results      []QACheckRunResult `json:"results"`
	PassedCount  int                `json:"passed_count"`
	FailedCount  int                `json:"failed_count"`
	SkippedCount int                `json:"skipped_count"`
}

// QACheckRunResult holds the result of a single QA check.
type QACheckRunResult struct {
	Name     string `json:"name"`
	Passed   bool   `json:"passed"`
	Skipped  bool   `json:"skipped"`
	ExitCode int    `json:"exit_code"`
	Duration string `json:"duration"`
	Output   string `json:"output,omitempty"`
}

// GetIssueMessage returns an issue message for a check.
func (r QACheckRunResult) GetIssueMessage() string {
	if r.Passed || r.Skipped {
		return ""
	}
	switch r.Name {
	case "audit":
		return i18n.T("i18n.done.find", "vulnerabilities")
	case "fmt":
		return i18n.T("i18n.done.find", "style issues")
	case "stan":
		return i18n.T("i18n.done.find", "analysis errors")
	case "psalm":
		return i18n.T("i18n.done.find", "type errors")
	case "test":
		return i18n.T("i18n.done.fail", "tests")
	case "rector":
		return i18n.T("i18n.done.find", "refactoring suggestions")
	case "infection":
		return i18n.T("i18n.fail.pass", "mutation testing")
	default:
		return i18n.T("i18n.done.find", "issues")
	}
}
