package plugins

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"strings"

	"github.com/muyue/muyue/internal/agent"
)

func DiscoverPlugins(paths []string) []*DiscoveredPlugin {
	var plugins []*DiscoveredPlugin

	for _, p := range paths {
		expanded := expandPath(p)
		info, err := os.Stat(expanded)
		if err != nil {
			continue
		}

		if info.IsDir() {
			entries, err := os.ReadDir(expanded)
			if err != nil {
				continue
			}
			for _, entry := range entries {
				if !entry.IsDir() {
					continue
				}
				pluginDir := filepath.Join(expanded, entry.Name())
				if dp := scanPluginDir(pluginDir); dp != nil {
					plugins = append(plugins, dp)
				}
			}
		}
	}

	return plugins
}

type DiscoveredPlugin struct {
	Name    string `json:"name"`
	Path    string `json:"path"`
	Type    string `json:"type"`
	Valid   bool   `json:"valid"`
	Error   string `json:"error,omitempty"`
}

func scanPluginDir(dir string) *DiscoveredPlugin {
	name := filepath.Base(dir)
	dp := &DiscoveredPlugin{
		Name: name,
		Path: dir,
	}

	initPy := filepath.Join(dir, "__init__.py")
	mainGo := filepath.Join(dir, "main.go")
	manifest := filepath.Join(dir, "plugin.json")

	if _, err := os.Stat(manifest); err == nil {
		dp.Type = "manifest"
		dp.Valid = true
		return dp
	}

	if _, err := os.Stat(mainGo); err == nil {
		dp.Type = "go"
		dp.Valid = true
		return dp
	}

	if _, err := os.Stat(initPy); err == nil {
		dp.Type = "python"
		dp.Valid = true
		return dp
	}

	executables := []string{name, name + ".sh"}
	for _, exe := range executables {
		fullPath := filepath.Join(dir, exe)
		if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
			dp.Type = "executable"
			dp.Valid = true
			dp.Path = fullPath
			return dp
		}
	}

	return dp
}

type PluginManifest struct {
	Name        string            `json:"name"`
	Version     string            `json:"version"`
	Description string            `json:"description"`
	Tools       []ManifestTool    `json:"tools,omitempty"`
	Hooks       []ManifestHook    `json:"hooks,omitempty"`
	Command     string            `json:"command,omitempty"`
	Args        []string          `json:"args,omitempty"`
	Env         map[string]string `json:"env,omitempty"`
}

type ManifestTool struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Params      json.RawMessage `json:"parameters"`
}

type ManifestHook struct {
	Type string `json:"type"`
}

func LoadManifest(path string) (*PluginManifest, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read manifest: %w", err)
	}

	var manifest PluginManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, fmt.Errorf("parse manifest: %w", err)
	}

	return &manifest, nil
}

func LoadExecutablePlugin(discovered *DiscoveredPlugin) (*Plugin, error) {
	if !discovered.Valid {
		return nil, fmt.Errorf("invalid plugin: %s", discovered.Name)
	}

	switch discovered.Type {
	case "manifest":
		return loadManifestPlugin(discovered)
	case "executable":
		return loadExecutableAsPlugin(discovered)
	default:
		return nil, fmt.Errorf("unsupported plugin type: %s", discovered.Type)
	}
}

func loadManifestPlugin(dp *DiscoveredPlugin) (*Plugin, error) {
	manifestPath := filepath.Join(dp.Path, "plugin.json")
	manifest, err := LoadManifest(manifestPath)
	if err != nil {
		return nil, err
	}

	p := NewPlugin(manifest.Name, manifest.Version, manifest.Description)

	for _, mt := range manifest.Tools {
		handler := createExternalHandler(dp.Path, manifest)
		td := &ToolDefinition{
			Name:        mt.Name,
			Description: mt.Description,
			Params:      mt.Params,
			Handler:     handler,
		}
		p.AddTool(td)
	}

	return p, nil
}

func loadExecutableAsPlugin(dp *DiscoveredPlugin) (*Plugin, error) {
	p := NewPlugin(dp.Name, "0.0.1", "Executable plugin: "+dp.Name)

	paramsSchema, _ := json.Marshal(map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"action": map[string]string{"type": "string", "description": "Action to execute"},
			"args":   map[string]string{"type": "object", "description": "Arguments for the action"},
		},
		"required": []string{"action"},
	})

	td := &ToolDefinition{
		Name:        dp.Name,
		Description: "External plugin tool: " + dp.Name,
		Params:      paramsSchema,
		Handler:     createScriptHandler(dp.Path),
	}
	p.AddTool(td)

	return p, nil
}

func createExternalHandler(pluginDir string, manifest *PluginManifest) func(ctx context.Context, raw json.RawMessage) (agent.ToolResponse, error) {
	return func(ctx context.Context, raw json.RawMessage) (agent.ToolResponse, error) {
		if manifest.Command == "" {
			return agent.TextErrorResponse(fmt.Sprintf("no command configured for plugin %s", manifest.Name)), nil
		}

		cmd := exec.CommandContext(ctx, manifest.Command, manifest.Args...)
		cmd.Dir = pluginDir
		cmd.Stdin = strings.NewReader(string(raw))

		output, err := cmd.CombinedOutput()
		if err != nil {
			return agent.TextErrorResponse(fmt.Sprintf("plugin execution failed: %v\n%s", err, string(output))), nil
		}

		return agent.TextResponse(string(output)), nil
	}
}

func createScriptHandler(scriptPath string) func(ctx context.Context, raw json.RawMessage) (agent.ToolResponse, error) {
	return func(ctx context.Context, raw json.RawMessage) (agent.ToolResponse, error) {
		cmd := exec.CommandContext(ctx, scriptPath)
		cmd.Stdin = strings.NewReader(string(raw))
		output, err := cmd.CombinedOutput()
		if err != nil {
			return agent.TextErrorResponse(fmt.Sprintf("script failed: %v\n%s", err, string(output))), nil
		}
		return agent.TextResponse(string(output)), nil
	}
}

func DefaultPluginPaths() []string {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil
	}

	configDir, err := configDir()
	if err != nil {
		return []string{filepath.Join(home, ".muyue", "plugins")}
	}

	return []string{
		filepath.Join(configDir, "plugins"),
		filepath.Join(home, ".muyue", "plugins"),
	}
}

func expandPath(p string) string {
	if strings.HasPrefix(p, "~/") {
		home, _ := os.UserHomeDir()
		return filepath.Join(home, p[2:])
	}
	return p
}

func configDir() (string, error) {
	configDir, err := os.UserConfigDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(configDir, "muyue"), nil
}

func generatePluginSchema(v interface{}) (json.RawMessage, error) {
	t := reflect.TypeOf(v)
	if t == nil {
		return json.RawMessage(`{"type":"object","properties":{}}`), nil
	}
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	if t.Kind() != reflect.Struct {
		return json.RawMessage(`{"type":"object","properties":{}}`), nil
	}

	props := make(map[string]interface{})
	required := []string{}

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}
		jsonTag := field.Tag.Get("json")
		if jsonTag == "-" {
			continue
		}
		jsonName := field.Name
		parts := strings.Split(jsonTag, ",")
		if parts[0] != "" {
			jsonName = parts[0]
		}
		omitempty := false
		for _, part := range parts[1:] {
			if part == "omitempty" {
				omitempty = true
			}
		}
		desc := field.Tag.Get("description")
		prop := map[string]interface{}{"type": goTypeToJSON(field.Type)}
		if desc != "" {
			prop["description"] = desc
		}
		props[jsonName] = prop
		if !omitempty {
			required = append(required, jsonName)
		}
	}

	schema := map[string]interface{}{
		"type":       "object",
		"properties": props,
	}
	if len(required) > 0 {
		schema["required"] = required
	}
	return json.Marshal(schema)
}

func goTypeToJSON(t reflect.Type) string {
	switch t.Kind() {
	case reflect.String:
		return "string"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return "integer"
	case reflect.Float32, reflect.Float64:
		return "number"
	case reflect.Bool:
		return "boolean"
	case reflect.Slice:
		if t.Elem().Kind() == reflect.Uint8 {
			return "string"
		}
		return "array"
	case reflect.Map:
		return "object"
	default:
		return "string"
	}
}
