package workbench

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

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

type Skill struct {
	ID               string     `json:"id"`
	Code             string     `json:"code"`
	Name             string     `json:"name"`
	Description      string     `json:"description"`
	Content          string     `json:"content"`
	Variables        []Variable `json:"variables"`
	ToolIDs          []string   `json:"tool_ids"`
	MCPServerIDs     []string   `json:"mcp_server_ids"`
	KnowledgeBaseIDs []string   `json:"knowledge_base_ids"`
	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 SkillInput struct {
	Code             string     `json:"code"`
	Name             string     `json:"name"`
	Description      string     `json:"description"`
	Content          string     `json:"content"`
	Variables        []Variable `json:"variables"`
	ToolIDs          []string   `json:"tool_ids"`
	MCPServerIDs     []string   `json:"mcp_server_ids"`
	KnowledgeBaseIDs []string   `json:"knowledge_base_ids"`
	Status           string     `json:"status"`
	CategoryID       *string    `json:"category_id,omitempty"`
	Tags             []string   `json:"tags"`
	DepartmentIDs    []string   `json:"department_ids"`
	Enabled          bool       `json:"enabled"`
}

// SkillService manages packaged capabilities (skills): prompt content plus
// optional tool / MCP server / knowledge-base bindings, publishable to the
// marketplace and consumed by digital employees.
type SkillService struct {
	assets *Service
}

func NewSkillService(assets *Service) *SkillService { return &SkillService{assets: assets} }

func (s *SkillService) validate(ctx context.Context, input *SkillInput, create bool) error {
	input.Code = strings.ToLower(strings.TrimSpace(input.Code))
	input.Name = strings.TrimSpace(input.Name)
	input.Description = strings.TrimSpace(input.Description)
	input.Content = strings.TrimSpace(input.Content)
	if !codeRE.MatchString(input.Code) || input.Name == "" || len(input.Name) > 128 || len(input.Description) > 4000 {
		return errors.New("Skill 编码、名称或描述格式无效")
	}
	if create && (input.Content == "" || len(input.Content) > 100000) {
		return errors.New("Skill 正文不能为空且最多 100000 字符")
	}
	switch input.Status {
	case "", "draft":
		input.Status = "draft"
	case "published", "archived":
	default:
		return errors.New("无效的资源状态")
	}
	var err error
	if input.Variables, err = validateVariables(input.Variables); 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, "skill")
	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 nil
}

const skillSelect = `SELECT s.id::text,s.code,s.name,s.description,s.content,s.variables,s.tool_ids,s.mcp_server_ids,s.knowledge_base_ids,s.status,s.category_id::text,coalesce(c.name,''),s.tags,s.department_ids::text[],s.enabled,s.revision,s.created_at,s.updated_at FROM gateway.skills s LEFT JOIN gateway.marketplace_categories c ON c.id=s.category_id`

func scanSkill(row pgx.Row) (Skill, error) {
	var s Skill
	var variables []byte
	err := row.Scan(&s.ID, &s.Code, &s.Name, &s.Description, &s.Content, &variables, &s.ToolIDs, &s.MCPServerIDs, &s.KnowledgeBaseIDs, &s.Status, &s.CategoryID, &s.CategoryName, &s.Tags, &s.DepartmentIDs, &s.Enabled, &s.Revision, &s.CreatedAt, &s.UpdatedAt)
	if err != nil {
		return s, mapNotFound(err)
	}
	_ = json.Unmarshal(variables, &s.Variables)
	return s, nil
}

func (s *SkillService) List(ctx context.Context) ([]Skill, error) {
	rows, err := s.assets.pool.Query(ctx, skillSelect+` ORDER BY s.updated_at DESC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Skill{}
	for rows.Next() {
		skill, err := scanSkill(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, skill)
	}
	return items, rows.Err()
}

func (s *SkillService) Get(ctx context.Context, id string) (Skill, error) {
	return scanSkill(s.assets.pool.QueryRow(ctx, skillSelect+` WHERE s.id=$1`, id))
}

func (s *SkillService) GetByCode(ctx context.Context, code string) (Skill, error) {
	return scanSkill(s.assets.pool.QueryRow(ctx, skillSelect+` WHERE s.code=$1`, code))
}

// GetPublishedByCode returns a published, enabled skill by code.
func (s *SkillService) GetPublishedByCode(ctx context.Context, code string) (Skill, error) {
	return scanSkill(s.assets.pool.QueryRow(ctx, skillSelect+` WHERE s.code=$1 AND s.status='published' AND s.enabled`, code))
}

func (s *SkillService) Save(ctx context.Context, id string, input SkillInput, actorID string, create bool) (Skill, error) {
	if err := s.validate(ctx, &input, create); err != nil {
		return Skill{}, err
	}
	tx, err := s.assets.pool.Begin(ctx)
	if err != nil {
		return Skill{}, err
	}
	defer rollback(ctx, tx)
	if create {
		id, err = newUUID()
		if err != nil {
			return Skill{}, err
		}
		variables, _ := json.Marshal(input.Variables)
		_, err = tx.Exec(ctx, `INSERT INTO gateway.skills(id,code,name,description,content,variables,tool_ids,mcp_server_ids,knowledge_base_ids,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)`, id, input.Code, input.Name, input.Description, input.Content, variables, input.ToolIDs, input.MCPServerIDs, input.KnowledgeBaseIDs, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled, actorID)
	} else {
		variables, _ := json.Marshal(input.Variables)
		tag, updateErr := tx.Exec(ctx, `UPDATE gateway.skills SET code=$2,name=$3,description=$4,content=$5,variables=$6,tool_ids=$7,mcp_server_ids=$8,knowledge_base_ids=$9,status=$10,category_id=$11,tags=$12,department_ids=$13,enabled=$14,revision=revision+1,updated_at=clock_timestamp() WHERE id=$1`, id, input.Code, input.Name, input.Description, input.Content, variables, input.ToolIDs, input.MCPServerIDs, input.KnowledgeBaseIDs, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled)
		err = updateErr
		if err == nil && tag.RowsAffected() == 0 {
			return Skill{}, ErrNotFound
		}
	}
	if err != nil {
		return Skill{}, err
	}
	event := "skill.updated"
	if create {
		event = "skill.created"
	}
	if err = emit(ctx, tx, event, "skill", id, actorID, nil); err != nil {
		return Skill{}, err
	}
	if err = tx.Commit(ctx); err != nil {
		return Skill{}, err
	}
	return s.Get(ctx, id)
}

func (s *SkillService) 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.digital_employees WHERE $1 = ANY(skill_ids)
		UNION ALL SELECT 1 FROM gateway.marketplace_installations WHERE resource_type='skill' AND resource_id=$1
	)`, id).Scan(&used); err != nil {
		return err
	}
	if used {
		return ErrConflict
	}
	tag, err := tx.Exec(ctx, `DELETE FROM gateway.skills WHERE id=$1`, id)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	if err = emit(ctx, tx, "skill.deleted", "skill", id, actorID, nil); err != nil {
		return err
	}
	return tx.Commit(ctx)
}

// Render applies the skill's variables to its content (same substitution as
// prompt rendering).
func (s *SkillService) Render(skill Skill, provided map[string]any) (string, error) {
	return RenderPrompt(skill.Content, skill.Variables, provided)
}
