package builder

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"ci/internal/model"
)

// LineCallback is called for each line of build output.
type LineCallback func(line string)

// BuildResult holds the outcome of a build.
type BuildResult struct {
	Success    bool
	LogPath    string
	CommitHash string
	Output     string
}

// Service handles build operations.
type Service struct{}

// New creates a new builder Service.
func New() *Service {
	return &Service{}
}

// Execute runs a build for the given project, streaming output via callback.
func (s *Service) Execute(ctx context.Context, project *model.Project, workDir, logsDir, commitHash string, onLine LineCallback) (*BuildResult, error) {
	// Create log file
	logFileName := fmt.Sprintf("build-%d.log", time.Now().UnixMilli())
	logPath := filepath.Join(logsDir, logFileName)
	logFile, err := os.Create(logPath)
	if err != nil {
		return nil, fmt.Errorf("failed to create log file: %w", err)
	}
	defer logFile.Close()

	writer := io.MultiWriter(logFile, &callbackWriter{fn: onLine})

	// Resolve the build script
	script := strings.TrimSpace(project.BuildScript)
	if script == "" {
		// Default: try go build
		script = "go build -o app ."
	}

	// Determine shell for the build script
	var cmd *exec.Cmd
	if isShellScript(script) {
		// Multi-line script or complex — write to temp file and execute
		scriptPath := filepath.Join(workDir, ".build-script.sh")
		if err := os.WriteFile(scriptPath, []byte("#!/bin/sh\nset -e\n"+script), 0755); err != nil {
			return nil, fmt.Errorf("failed to write build script: %w", err)
		}
		cmd = exec.CommandContext(ctx, "sh", scriptPath)
	} else {
		cmd = exec.CommandContext(ctx, "sh", "-c", script)
	}

	cmd.Dir = workDir
	cmd.Env = os.Environ()
	cmd.Stdout = writer
	cmd.Stderr = writer

	fmt.Fprintf(writer, "=== Build started at %s ===\n", time.Now().Format(time.RFC3339))
	fmt.Fprintf(writer, "Working directory: %s\n", workDir)
	fmt.Fprintf(writer, "Build script: %s\n", script)
	fmt.Fprintf(writer, "Commit: %s\n\n", commitHash)

	startTime := time.Now()
	err = cmd.Run()
	duration := time.Since(startTime)

	result := &BuildResult{
		LogPath:    logPath,
		CommitHash: commitHash,
	}

	if err != nil {
		fmt.Fprintf(writer, "\n=== Build FAILED after %s ===\n", duration.Round(time.Second))
		fmt.Fprintf(writer, "Error: %v\n", err)
		result.Success = false
		result.Output = fmt.Sprintf("Build failed: %v", err)
	} else {
		fmt.Fprintf(writer, "\n=== Build SUCCESS after %s ===\n", duration.Round(time.Second))
		result.Success = true
		result.Output = fmt.Sprintf("Build succeeded in %s", duration.Round(time.Second))
	}

	return result, nil
}

// ReadLog reads a build log file and returns its contents.
func (s *Service) ReadLog(logPath string) (string, error) {
	data, err := os.ReadFile(logPath)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// ReadLogTail reads the last n lines from a log file.
func (s *Service) ReadLogTail(logPath string, lines int) ([]string, error) {
	file, err := os.Open(logPath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var result []string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		result = append(result, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	if len(result) > lines {
		result = result[len(result)-lines:]
	}
	return result, nil
}

// isShellScript checks if the script looks multi-line or complex enough to need a temp file.
func isShellScript(script string) bool {
	return strings.Contains(script, "\n") || len(script) > 200
}

// callbackWriter implements io.Writer, calling a function for each line written.
type callbackWriter struct {
	fn    LineCallback
	buf   []byte
}

func (w *callbackWriter) Write(p []byte) (n int, err error) {
	if w.fn != nil {
		w.buf = append(w.buf, p...)
		for {
			idx := indexOfByte(w.buf, '\n')
			if idx < 0 {
				break
			}
			line := string(w.buf[:idx])
			if strings.HasSuffix(line, "\r") {
				line = line[:len(line)-1]
			}
			w.fn(line)
			w.buf = w.buf[idx+1:]
		}
	}
	return len(p), nil
}

func (w *callbackWriter) flush() {
	if w.fn != nil && len(w.buf) > 0 {
		w.fn(string(w.buf))
		w.buf = nil
	}
}

func indexOfByte(data []byte, b byte) int {
	for i, c := range data {
		if c == b {
			return i
		}
	}
	return -1
}
