package workbench

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"
)

type DigitalEmployee struct {
	ID               string    `json:"id"`
	Code             string    `json:"code"`
	Name             string    `json:"name"`
	Description      string    `json:"description"`
	Persona          string    `json:"persona"`
	Model            string    `json:"model"`
	SkillIDs         []string  `json:"skill_ids"`
	ToolIDs          []string  `json:"tool_ids"`
	MCPServerIDs     []string  `json:"mcp_server_ids"`
	KnowledgeBaseIDs []string  `json:"knowledge_base_ids"`
	Temperature      float64   `json:"temperature"`
	RetrievalTopK    int       `json:"retrieval_top_k"`
	MaxToolRounds    int       `json:"max_tool_rounds"`
	Status           string    `json:"status"`
	CategoryID       *string   `json:"category_id,omitempty"`
	CategoryName     string    `json:"category_name"`
	Tags             []string  `json:"tags"`
	DepartmentIDs    []string  `json:"department_ids"`
	Enabled          bool      `json:"enabled"`
	Revision         int64     `json:"revision"`
	CreatedAt        time.Time `json:"created_at"`
	UpdatedAt        time.Time `json:"updated_at"`
}

type DigitalEmployeeInput struct {
	Code             string   `json:"code"`
	Name             string   `json:"name"`
	Description      string   `json:"description"`
	Persona          string   `json:"persona"`
	Model            string   `json:"model"`
	SkillIDs         []string `json:"skill_ids"`
	ToolIDs          []string `json:"tool_ids"`
	MCPServerIDs     []string `json:"mcp_server_ids"`
	KnowledgeBaseIDs []string `json:"knowledge_base_ids"`
	Temperature      float64  `json:"temperature"`
	RetrievalTopK    int      `json:"retrieval_top_k"`
	MaxToolRounds    int      `json:"max_tool_rounds"`
	Status           string   `json:"status"`
	CategoryID       *string  `json:"category_id,omitempty"`
	Tags             []string `json:"tags"`
	DepartmentIDs    []string `json:"department_ids"`
	Enabled          bool     `json:"enabled"`
}

type DigitalEmployeeRun struct {
	ID              string    `json:"id"`
	DigitalEmployee string    `json:"digital_employee"`
	RequestID       string    `json:"request_id"`
	Status          string    `json:"status"`
	Error           string    `json:"error"`
	LatencyMS       int64     `json:"latency_ms"`
	RetrievalCount  int       `json:"retrieval_count"`
	ToolCount       int       `json:"tool_count"`
	CreatedAt       time.Time `json:"created_at"`
}

// DigitalEmployeeService manages composite digital employees. A digital
// employee bundles a persona, model, and bound skills / tools / MCP servers /
// knowledge bases; it is the end-user consumption point of the marketplace.
type DigitalEmployeeService struct {
	assets     *Service
	skills     *SkillService
	tools      *ToolService
	mcpServers *MCPServerService
}

func NewDigitalEmployeeService(assets *Service, skills *SkillService, tools *ToolService, mcpServers *MCPServerService) *DigitalEmployeeService {
	return &DigitalEmployeeService{assets: assets, skills: skills, tools: tools, mcpServers: mcpServers}
}

func (s *DigitalEmployeeService) validate(ctx context.Context, input *DigitalEmployeeInput, create bool) error {
	input.Code = strings.ToLower(strings.TrimSpace(input.Code))
	input.Name = strings.TrimSpace(input.Name)
	input.Description = strings.TrimSpace(input.Description)
	input.Persona = strings.TrimSpace(input.Persona)
	input.Model = strings.TrimSpace(input.Model)
	if !codeRE.MatchString(input.Code) || input.Name == "" || len(input.Name) > 128 || len(input.Description) > 4000 || len(input.Persona) > 100000 {
		return errors.New("数字员工编码、名称、描述或人设格式无效")
	}
	if input.Model == "" {
		return errors.New("必须指定模型")
	}
	if input.Temperature == 0 {
		input.Temperature = 0.7
	}
	if input.Temperature < 0 || input.Temperature > 2 {
		return errors.New("temperature 应在 0-2 之间")
	}
	if input.RetrievalTopK == 0 {
		input.RetrievalTopK = 5
	}
	if input.RetrievalTopK < 1 || input.RetrievalTopK > 50 {
		return errors.New("retrieval_top_k 应在 1-50 之间")
	}
	if input.MaxToolRounds == 0 {
		input.MaxToolRounds = 5
	}
	if input.MaxToolRounds < 1 || input.MaxToolRounds > 20 {
		return errors.New("max_tool_rounds 应在 1-20 之间")
	}
	switch input.Status {
	case "", "draft":
		input.Status = "draft"
	case "published", "archived":
	default:
		return errors.New("无效的资源状态")
	}
	var err error
	if input.SkillIDs, err = normalizeStrings(input.SkillIDs, 100); err != nil {
		return err
	}
	if input.ToolIDs, err = normalizeStrings(input.ToolIDs, 100); err != nil {
		return err
	}
	if input.MCPServerIDs, err = normalizeStrings(input.MCPServerIDs, 100); err != nil {
		return err
	}
	if input.KnowledgeBaseIDs, err = normalizeStrings(input.KnowledgeBaseIDs, 100); err != nil {
		return err
	}
	if input.CategoryID != nil && strings.TrimSpace(*input.CategoryID) == "" {
		input.CategoryID = nil
	}
	var categoryErr error
	input.CategoryID, categoryErr = validCategoryID(ctx, s.assets.pool, input.CategoryID, "digital_employee")
	if categoryErr != nil {
		return categoryErr
	}
	if input.Tags, err = normalizeStrings(input.Tags, 30); err != nil {
		return err
	}
	if input.DepartmentIDs, err = normalizeStrings(input.DepartmentIDs, 100); err != nil {
		return err
	}
	return s.validateBindings(ctx, input, false)
}

// validateBindings ensures every bound asset exists (and, when strict, is
// enabled) so a published digital employee never references a missing resource.
func (s *DigitalEmployeeService) validateBindings(ctx context.Context, input *DigitalEmployeeInput, strict bool) error {
	for _, id := range input.SkillIDs {
		skill, err := s.skills.Get(ctx, id)
		if err != nil {
			return fmt.Errorf("绑定的 Skill %s 不存在", id)
		}
		if strict && !skill.Enabled {
			return fmt.Errorf("绑定的 Skill %s 未启用", skill.Code)
		}
	}
	for _, id := range input.ToolIDs {
		tool, err := s.tools.Get(ctx, id)
		if err != nil {
			return fmt.Errorf("绑定的工具 %s 不存在", id)
		}
		if strict && !tool.Enabled {
			return fmt.Errorf("绑定的工具 %s 未启用", tool.Code)
		}
	}
	for _, id := range input.MCPServerIDs {
		server, err := s.mcpServers.Get(ctx, id)
		if err != nil {
			return fmt.Errorf("绑定的 MCP 服务器 %s 不存在", id)
		}
		if strict && !server.Enabled {
			return fmt.Errorf("绑定的 MCP 服务器 %s 未启用", server.Code)
		}
	}
	for _, id := range input.KnowledgeBaseIDs {
		var exists bool
		if err := s.assets.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.knowledge_bases WHERE id=$1)`, id).Scan(&exists); err != nil {
			return err
		}
		if !exists {
			return fmt.Errorf("绑定的知识库 %s 不存在", id)
		}
		if strict {
			var enabled bool
			if err := s.assets.pool.QueryRow(ctx, `SELECT enabled FROM gateway.knowledge_bases WHERE id=$1`, id).Scan(&enabled); err != nil {
				return err
			}
			if !enabled {
				return fmt.Errorf("绑定的知识库 %s 未启用", id)
			}
		}
	}
	return nil
}

const digitalEmployeeSelect = `SELECT d.id::text,d.code,d.name,d.description,d.persona,d.model,d.skill_ids,d.tool_ids,d.mcp_server_ids,d.knowledge_base_ids,d.temperature,d.retrieval_top_k,d.max_tool_rounds,d.status,d.category_id::text,coalesce(c.name,''),d.tags,d.department_ids::text[],d.enabled,d.revision,d.created_at,d.updated_at FROM gateway.digital_employees d LEFT JOIN gateway.marketplace_categories c ON c.id=d.category_id`

func scanDigitalEmployee(row pgx.Row) (DigitalEmployee, error) {
	var d DigitalEmployee
	err := row.Scan(&d.ID, &d.Code, &d.Name, &d.Description, &d.Persona, &d.Model, &d.SkillIDs, &d.ToolIDs, &d.MCPServerIDs, &d.KnowledgeBaseIDs, &d.Temperature, &d.RetrievalTopK, &d.MaxToolRounds, &d.Status, &d.CategoryID, &d.CategoryName, &d.Tags, &d.DepartmentIDs, &d.Enabled, &d.Revision, &d.CreatedAt, &d.UpdatedAt)
	return d, mapNotFound(err)
}

func (s *DigitalEmployeeService) List(ctx context.Context) ([]DigitalEmployee, error) {
	rows, err := s.assets.pool.Query(ctx, digitalEmployeeSelect+` ORDER BY d.updated_at DESC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []DigitalEmployee{}
	for rows.Next() {
		item, err := scanDigitalEmployee(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *DigitalEmployeeService) Get(ctx context.Context, id string) (DigitalEmployee, error) {
	return scanDigitalEmployee(s.assets.pool.QueryRow(ctx, digitalEmployeeSelect+` WHERE d.id=$1`, id))
}

func (s *DigitalEmployeeService) GetByCode(ctx context.Context, code string) (DigitalEmployee, error) {
	return scanDigitalEmployee(s.assets.pool.QueryRow(ctx, digitalEmployeeSelect+` WHERE d.code=$1`, code))
}

// GetPublishedByCode returns a published, enabled digital employee by code.
func (s *DigitalEmployeeService) GetPublishedByCode(ctx context.Context, code string) (DigitalEmployee, error) {
	return scanDigitalEmployee(s.assets.pool.QueryRow(ctx, digitalEmployeeSelect+` WHERE d.code=$1 AND d.status='published' AND d.enabled`, code))
}

func (s *DigitalEmployeeService) Save(ctx context.Context, id string, input DigitalEmployeeInput, actorID string, create bool) (DigitalEmployee, error) {
	if err := s.validate(ctx, &input, create); err != nil {
		return DigitalEmployee{}, err
	}
	tx, err := s.assets.pool.Begin(ctx)
	if err != nil {
		return DigitalEmployee{}, err
	}
	defer rollback(ctx, tx)
	if create {
		id, err = newUUID()
		if err != nil {
			return DigitalEmployee{}, err
		}
		_, err = tx.Exec(ctx, `INSERT INTO gateway.digital_employees(id,code,name,description,persona,model,skill_ids,tool_ids,mcp_server_ids,knowledge_base_ids,temperature,retrieval_top_k,max_tool_rounds,status,category_id,tags,department_ids,enabled,created_by) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)`, id, input.Code, input.Name, input.Description, input.Persona, input.Model, input.SkillIDs, input.ToolIDs, input.MCPServerIDs, input.KnowledgeBaseIDs, input.Temperature, input.RetrievalTopK, input.MaxToolRounds, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled, actorID)
	} else {
		tag, updateErr := tx.Exec(ctx, `UPDATE gateway.digital_employees SET code=$2,name=$3,description=$4,persona=$5,model=$6,skill_ids=$7,tool_ids=$8,mcp_server_ids=$9,knowledge_base_ids=$10,temperature=$11,retrieval_top_k=$12,max_tool_rounds=$13,status=$14,category_id=$15,tags=$16,department_ids=$17,enabled=$18,revision=revision+1,updated_at=clock_timestamp() WHERE id=$1`, id, input.Code, input.Name, input.Description, input.Persona, input.Model, input.SkillIDs, input.ToolIDs, input.MCPServerIDs, input.KnowledgeBaseIDs, input.Temperature, input.RetrievalTopK, input.MaxToolRounds, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled)
		err = updateErr
		if err == nil && tag.RowsAffected() == 0 {
			return DigitalEmployee{}, ErrNotFound
		}
	}
	if err != nil {
		return DigitalEmployee{}, err
	}
	event := "digital_employee.updated"
	if create {
		event = "digital_employee.created"
	}
	if err = emit(ctx, tx, event, "digital_employee", id, actorID, nil); err != nil {
		return DigitalEmployee{}, err
	}
	if err = tx.Commit(ctx); err != nil {
		return DigitalEmployee{}, err
	}
	return s.Get(ctx, id)
}

// Publish revalidates all bindings (must exist and be enabled) and flips the
// status to published so the resource appears in the marketplace.
func (s *DigitalEmployeeService) Publish(ctx context.Context, id, actorID string) (DigitalEmployee, error) {
	current, err := s.Get(ctx, id)
	if err != nil {
		return DigitalEmployee{}, err
	}
	input := DigitalEmployeeInput{
		Code: current.Code, Name: current.Name, Description: current.Description,
		Persona: current.Persona, Model: current.Model,
		SkillIDs: current.SkillIDs, ToolIDs: current.ToolIDs, MCPServerIDs: current.MCPServerIDs,
		KnowledgeBaseIDs: current.KnowledgeBaseIDs, Temperature: current.Temperature,
		RetrievalTopK: current.RetrievalTopK, MaxToolRounds: current.MaxToolRounds,
		Status: "published", CategoryID: current.CategoryID, Tags: current.Tags,
		DepartmentIDs: current.DepartmentIDs, Enabled: current.Enabled,
	}
	if err = s.validateBindings(ctx, &input, true); err != nil {
		return DigitalEmployee{}, err
	}
	if err = s.setStatus(ctx, id, actorID, "published"); err != nil {
		return DigitalEmployee{}, err
	}
	return s.Get(ctx, id)
}

// Archive unpublishes a digital employee from the marketplace without deleting
// its configuration.
func (s *DigitalEmployeeService) Archive(ctx context.Context, id, actorID string) (DigitalEmployee, error) {
	if err := s.setStatus(ctx, id, actorID, "archived"); err != nil {
		return DigitalEmployee{}, err
	}
	return s.Get(ctx, id)
}

func (s *DigitalEmployeeService) setStatus(ctx context.Context, id, actorID, status string) error {
	tx, err := s.assets.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer rollback(ctx, tx)
	tag, err := tx.Exec(ctx, `UPDATE gateway.digital_employees SET status=$2,revision=revision+1,updated_at=clock_timestamp() WHERE id=$1`, id, status)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	if err = emit(ctx, tx, "digital_employee."+status, "digital_employee", id, actorID, nil); err != nil {
		return err
	}
	return tx.Commit(ctx)
}

func (s *DigitalEmployeeService) Delete(ctx context.Context, id, actorID string) error {
	tx, err := s.assets.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer rollback(ctx, tx)
	var used bool
	if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.marketplace_installations WHERE resource_type='digital_employee' AND resource_id=$1)`, id).Scan(&used); err != nil {
		return err
	}
	if used {
		return ErrConflict
	}
	tag, err := tx.Exec(ctx, `DELETE FROM gateway.digital_employees WHERE id=$1`, id)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	if err = emit(ctx, tx, "digital_employee.deleted", "digital_employee", id, actorID, nil); err != nil {
		return err
	}
	return tx.Commit(ctx)
}

func (s *DigitalEmployeeService) ListRuns(ctx context.Context, id string, limit int) ([]DigitalEmployeeRun, error) {
	if limit < 1 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}
	rows, err := s.assets.pool.Query(ctx, `SELECT id::text,(SELECT code FROM gateway.digital_employees WHERE id=r.digital_employee_id),request_id,status,error,latency_ms,retrieval_count,tool_count,created_at FROM gateway.digital_employee_runs r WHERE digital_employee_id=$1 ORDER BY created_at DESC LIMIT $2`, id, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []DigitalEmployeeRun{}
	for rows.Next() {
		var run DigitalEmployeeRun
		if err = rows.Scan(&run.ID, &run.DigitalEmployee, &run.RequestID, &run.Status, &run.Error, &run.LatencyMS, &run.RetrievalCount, &run.ToolCount, &run.CreatedAt); err != nil {
			return nil, err
		}
		items = append(items, run)
	}
	return items, rows.Err()
}
