package service

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sync"
	"time"

	"ci/internal/builder"
	"ci/internal/deployer"
	gitsvc "ci/internal/git"
	"ci/internal/model"
	"ci/internal/process"
	"ci/internal/store"
	"ci/internal/workspace"
)

// ProjectService handles all business logic for project management.
type ProjectService struct {
	Store     *store.Store
	Workspace *workspace.Manager
	Git       *gitsvc.Service
	Builder   *builder.Service
	Process   *process.Manager
	Deployer  *deployer.Service

	// SSE subscribers for log streaming
	mu          sync.RWMutex
	subscribers map[uint][]chan string // projectID -> list of subscriber channels
}

// NewProjectService creates a new ProjectService.
func NewProjectService(s *store.Store, w *workspace.Manager) *ProjectService {
	return &ProjectService{
		Store:       s,
		Workspace:   w,
		Git:         gitsvc.New(),
		Builder:     builder.New(),
		Process:     process.NewManager(),
		Deployer:    deployer.New(),
		subscribers: make(map[uint][]chan string),
	}
}

// CreateProject creates a new project and initializes its workspace.
func (s *ProjectService) CreateProject(p *model.Project) error {
	p.Status = "stopped"
	if err := s.Store.CreateProject(p); err != nil {
		return err
	}
	return s.Workspace.InitProject(p.ID)
}

// ListProjects returns all projects.
func (s *ProjectService) ListProjects() ([]model.Project, error) {
	projects, err := s.Store.ListProjects()
	if err != nil {
		return nil, err
	}
	// Update status from process manager
	for i := range projects {
		status := s.Process.GetStatus(projects[i].ID)
		if status == "running" {
			projects[i].Status = "running"
		} else if projects[i].Status == "running" {
			projects[i].Status = "stopped"
		}
	}
	return projects, nil
}

// GetProject returns a single project.
func (s *ProjectService) GetProject(id uint) (*model.Project, error) {
	p, err := s.Store.GetProject(id)
	if err != nil {
		return nil, err
	}
	status := s.Process.GetStatus(id)
	if status == "running" {
		p.Status = "running"
	} else if p.Status == "running" {
		p.Status = "stopped"
	}
	return p, nil
}

// UpdateProject updates a project's configuration.
func (s *ProjectService) UpdateProject(p *model.Project) error {
	return s.Store.UpdateProject(p)
}

// DeleteProject deletes a project, stops its process, and cleans up its workspace.
func (s *ProjectService) DeleteProject(id uint) error {
	s.Process.Stop(id)
	s.Workspace.CleanProject(id)
	s.Store.DeleteDeployConfig(id) // clean up deploy config if exists
	return s.Store.DeleteProject(id)
}

// BuildProject triggers a build for the project.
func (s *ProjectService) BuildProject(ctx context.Context, projectID uint) (*model.BuildRecord, error) {
	project, err := s.Store.GetProject(projectID)
	if err != nil {
		return nil, err
	}

	// Start a build record
	now := time.Now()
	record := &model.BuildRecord{
		ProjectID: projectID,
		Status:    "running",
		StartedAt: now,
	}
	if err := s.Store.CreateBuildRecord(record); err != nil {
		return nil, err
	}

	// Update project status
	project.Status = "building"
	s.Store.UpdateProject(project)

	// For git mode, pull/clone first
	var commitHash string
	if project.Mode == "git" && project.GitURL != "" {
		srcDir := s.Workspace.SrcDir(projectID)
		hash, err := s.Git.CloneOrPull(project.GitURL, project.GitBranch, project.GitUsername, project.GitPassword, srcDir)
		if err != nil {
			s.finishBuild(record, project, "failed", err.Error(), "")
			return record, err
		}
		commitHash = hash
		record.CommitHash = hash
		s.Store.UpdateBuildRecord(record)
	}

	// Execute build
	srcDir := s.Workspace.SrcDir(projectID)
	logsDir := s.Workspace.LogsDir(projectID)

	// Notify SSE subscribers about the build starting
	s.broadcast(projectID, fmt.Sprintf("[Build #%d] Starting build for project %s...", record.ID, project.Name))

	result, err := s.Builder.Execute(ctx, project, srcDir, logsDir, commitHash, func(line string) {
		s.broadcast(projectID, line)
	})

	if err != nil || !result.Success {
		errMsg := "build failed"
		if err != nil {
			errMsg = err.Error()
		}
		if result != nil && result.Output != "" {
			errMsg = result.Output
		}
		s.finishBuild(record, project, "failed", errMsg, result.LogPath)
		if err != nil {
			return record, err
		}
		return record, fmt.Errorf("%s", errMsg)
	}

	finishTime := time.Now()
	record.Status = "success"
	record.LogPath = result.LogPath
	record.CommitHash = result.CommitHash
	record.FinishedAt = &finishTime
	s.Store.UpdateBuildRecord(record)

	project.Status = "stopped"
	s.Store.UpdateProject(project)

	s.broadcast(projectID, fmt.Sprintf("[Build #%d] Build completed successfully", record.ID))

	// Auto-deploy if configured
	if autoCfg, _ := s.Store.GetDeployConfig(projectID); autoCfg != nil && autoCfg.AutoDeploy {
		s.broadcast(projectID, "[Deploy] Auto-deploy enabled, triggering deployment...")
		go func() {
			depCtx, depCancel := context.WithTimeout(context.Background(), 10*time.Minute)
			defer depCancel()
			s.TriggerDeploy(depCtx, projectID, &record.ID)
		}()
	}

	return record, nil
}

// StartProject starts the built binary for a project.
func (s *ProjectService) StartProject(ctx context.Context, projectID uint) error {
	project, err := s.Store.GetProject(projectID)
	if err != nil {
		return err
	}

	if s.Process.GetStatus(projectID) == "running" {
		return fmt.Errorf("project %s is already running", project.Name)
	}

	// Parse env vars from JSON string
	var envVars map[string]string
	if project.EnvVars != "" {
		json.Unmarshal([]byte(project.EnvVars), &envVars)
	}
	if envVars == nil {
		envVars = make(map[string]string)
	}

	// Determine working directory
	workDir := s.Workspace.SrcDir(projectID)

	// Determine the command to run
	runCommand := project.RunCommand
	if runCommand == "" {
		if project.OutputBinary != "" {
			runCommand = "./" + project.OutputBinary
		} else if project.Mode == "git" {
			runCommand = "./app"
		} else {
			return fmt.Errorf("no run command configured")
		}
	}

	// If port is specified, add it as an env var (PORT)
	if project.Port > 0 {
		envVars["PORT"] = fmt.Sprintf("%d", project.Port)
	}

	logsDir := s.Workspace.LogsDir(projectID)
	logFileName := fmt.Sprintf("%s/run-%d.log", logsDir, time.Now().UnixMilli())

	// Create log file for run output
	logFile, err := os.Create(logFileName)
	if err != nil {
		return fmt.Errorf("failed to create log file: %w", err)
	}

	mp, err := s.Process.Start(projectID, workDir, runCommand, envVars, logFile)
	if err != nil {
		return err
	}

	// Subscribe to process output for SSE
	mp.Subscribe(func(line string) {
		s.broadcast(projectID, line)
	})

	project.Status = "running"
	project.PID = mp.PID
	s.Store.UpdateProject(project)

	s.broadcast(projectID, fmt.Sprintf("[Process] Started with PID %d: %s", mp.PID, runCommand))
	return nil
}

// StopProject stops a running project process.
func (s *ProjectService) StopProject(projectID uint) error {
	if err := s.Process.Stop(projectID); err != nil {
		return err
	}

	project, err := s.Store.GetProject(projectID)
	if err == nil {
		project.Status = "stopped"
		project.PID = 0
		s.Store.UpdateProject(project)
	}

	s.broadcast(projectID, "[Process] Stopped")
	return nil
}

// RestartProject stops and restarts a project.
func (s *ProjectService) RestartProject(ctx context.Context, projectID uint) error {
	s.StopProject(projectID)
	// Brief pause to ensure port is freed
	time.Sleep(500 * time.Millisecond)
	return s.StartProject(ctx, projectID)
}

// GetBuildHistory returns the build history for a project.
func (s *ProjectService) GetBuildHistory(projectID uint, limit int) ([]model.BuildRecord, error) {
	return s.Store.ListBuildRecords(projectID, limit)
}

// GetBuildLog returns the contents of a build log.
func (s *ProjectService) GetBuildLog(buildID uint) (string, error) {
	record, err := s.Store.GetBuildRecord(buildID)
	if err != nil {
		return "", err
	}
	return s.Builder.ReadLog(record.LogPath)
}

// --- Deploy Config ---

// GetDeployConfig returns the deploy config for a project, or nil.
func (s *ProjectService) GetDeployConfig(projectID uint) (*model.DeployConfig, error) {
	return s.Store.GetDeployConfig(projectID)
}

// UpdateDeployConfig creates or updates the deploy config for a project.
func (s *ProjectService) UpdateDeployConfig(cfg *model.DeployConfig) error {
	// Preserve sensitive fields if not sent
	if existing, err := s.Store.GetDeployConfig(cfg.ProjectID); err == nil && existing != nil {
		if cfg.SSHKey == "" {
			cfg.SSHKey = existing.SSHKey
		}
		if cfg.SSHPassword == "" {
			cfg.SSHPassword = existing.SSHPassword
		}
	}
	return s.Store.UpsertDeployConfig(cfg)
}

// DeleteDeployConfig removes the deploy config for a project.
func (s *ProjectService) DeleteDeployConfig(projectID uint) error {
	return s.Store.DeleteDeployConfig(projectID)
}

// --- Deploy Execution ---

// TriggerDeploy starts a deployment for the project.
func (s *ProjectService) TriggerDeploy(ctx context.Context, projectID uint, buildRecordID *uint) (*model.DeployRecord, error) {
	_, err := s.Store.GetProject(projectID)
	if err != nil {
		return nil, fmt.Errorf("project not found: %w", err)
	}

	cfg, err := s.Store.GetDeployConfig(projectID)
	if err != nil || cfg == nil {
		return nil, fmt.Errorf("deploy config not found for project %d", projectID)
	}

	// Create deploy record
	now := time.Now()
	record := &model.DeployRecord{
		ProjectID:     projectID,
		BuildRecordID: buildRecordID,
		Status:        "running",
		StartedAt:     now,
	}
	if err := s.Store.CreateDeployRecord(record); err != nil {
		return nil, err
	}

	// Log file path
	logsDir := s.Workspace.LogsDir(projectID)
	logFileName := fmt.Sprintf("deploy-%d.log", time.Now().UnixMilli())
	logPath := filepath.Join(logsDir, logFileName)

	// Parse file mappings
	var mappings []deployer.FileMapping
	if cfg.FileMappings != "" {
		if err := json.Unmarshal([]byte(cfg.FileMappings), &mappings); err != nil {
			s.finishDeploy(record, "failed", fmt.Sprintf("invalid file mappings: %v", err), logPath)
			return record, err
		}
	}

	// Resolve local file paths relative to src dir
	srcDir := s.Workspace.SrcDir(projectID)
	for i, m := range mappings {
		if !filepath.IsAbs(m.Local) {
			mappings[i].Local = filepath.Join(srcDir, m.Local)
		}
	}

	// Build deployer config
	dCfg := &deployer.Config{
		Host:       cfg.Host,
		Port:       cfg.Port,
		Username:   cfg.Username,
		AuthMethod: cfg.AuthMethod,
		SSHKey:     cfg.SSHKey,
		Password:   cfg.SSHPassword,
	}

	// Create log file
	logFile, err := os.Create(logPath)
	if err != nil {
		s.finishDeploy(record, "failed", err.Error(), logPath)
		return record, err
	}
	defer logFile.Close()

	// MultiWriter: write to both log file and SSE broadcast
	writer := io.MultiWriter(logFile, &deployLogWriter{fn: func(line string) {
		s.broadcast(projectID, "[Deploy] "+line)
	}})

	s.broadcast(projectID, fmt.Sprintf("[Deploy #%d] Starting deployment to %s@%s:%d...",
		record.ID, cfg.Username, cfg.Host, cfg.Port))

	// Execute deployment
	result, err := s.Deployer.Deploy(ctx, dCfg, cfg.DeployDir, mappings,
		cfg.PreDeployCommand, cfg.PostDeployCommand,
		cfg.HealthCheckURL, cfg.HealthCheckTimeout,
		func(line string) {
			fmt.Fprintln(writer, line)
		})

	if err != nil || !result.Success {
		errMsg := "deploy failed"
		if err != nil {
			errMsg = err.Error()
		}
		if result != nil && result.Output != "" {
			errMsg = result.Output
		}
		s.finishDeploy(record, "failed", errMsg, logPath)
		if err != nil {
			return record, err
		}
		return record, fmt.Errorf("%s", errMsg)
	}

	// Success
	finishTime := time.Now()
	record.Status = "success"
	record.LogPath = logPath
	record.FinishedAt = &finishTime
	s.Store.UpdateDeployRecord(record)

	s.broadcast(projectID, fmt.Sprintf("[Deploy #%d] Deployment completed successfully", record.ID))
	return record, nil
}

// GetDeployHistory returns deploy history for a project.
func (s *ProjectService) GetDeployHistory(projectID uint, limit int) ([]model.DeployRecord, error) {
	return s.Store.ListDeployRecords(projectID, limit)
}

// GetDeployLog returns the full log for a specific deployment.
func (s *ProjectService) GetDeployLog(deployID uint) (string, error) {
	record, err := s.Store.GetDeployRecord(deployID)
	if err != nil {
		return "", err
	}
	data, err := os.ReadFile(record.LogPath)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// deployLogWriter implements io.Writer for SSE broadcasting.
type deployLogWriter struct {
	fn  func(string)
	buf []byte
}

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

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

// SSE

// SubscribeLogs registers a channel to receive log lines for a project.
func (s *ProjectService) SubscribeLogs(projectID uint) chan string {
	ch := make(chan string, 100)
	s.mu.Lock()
	s.subscribers[projectID] = append(s.subscribers[projectID], ch)
	s.mu.Unlock()
	return ch
}

// UnsubscribeLogs removes a subscriber channel for a project.
func (s *ProjectService) UnsubscribeLogs(projectID uint, ch chan string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	subs := s.subscribers[projectID]
	for i, sub := range subs {
		if sub == ch {
			s.subscribers[projectID] = append(subs[:i], subs[i+1:]...)
			close(ch)
			break
		}
	}
}

func (s *ProjectService) broadcast(projectID uint, line string) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, ch := range s.subscribers[projectID] {
		select {
		case ch <- line:
		default:
			// Drop if channel is full
		}
	}
}

// Shutdown gracefully stops all running processes.
func (s *ProjectService) Shutdown() {
	s.Process.Shutdown()
}

// helper

func (s *ProjectService) finishBuild(record *model.BuildRecord, project *model.Project, status, errMsg, logPath string) {
	now := time.Now()
	record.Status = status
	record.FinishedAt = &now
	if logPath != "" {
		record.LogPath = logPath
	}
	s.Store.UpdateBuildRecord(record)
	project.Status = "error"
	s.Store.UpdateProject(project)
	s.broadcast(project.ID, fmt.Sprintf("[Build #%d] %s", record.ID, errMsg))
}

func (s *ProjectService) finishDeploy(record *model.DeployRecord, status, errMsg, logPath string) {
	now := time.Now()
	record.Status = status
	record.FinishedAt = &now
	if logPath != "" {
		record.LogPath = logPath
	}
	s.Store.UpdateDeployRecord(record)
	s.broadcast(record.ProjectID, fmt.Sprintf("[Deploy #%d] %s", record.ID, errMsg))
}
