package module

import (
	"bytes"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v3"
)

// LoadFile parses a single module.yaml at path.
func LoadFile(path string) (*Manifest, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read %s: %w", path, err)
	}
	var m Manifest
	dec := yaml.NewDecoder(bytes.NewReader(data))
	dec.KnownFields(true)
	if err := dec.Decode(&m); err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}
	if err := m.Validate(); err != nil {
		return nil, fmt.Errorf("validate %s: %w", path, err)
	}
	abs, err := filepath.Abs(path)
	if err != nil {
		return nil, fmt.Errorf("abs %s: %w", path, err)
	}
	m.Path = abs
	m.Dir = filepath.Dir(abs)
	return &m, nil
}

// LoadDir scans dir for subdirectories containing a module.yaml and loads each.
// Subdirectories without module.yaml are skipped silently.
func LoadDir(dir string) ([]*Manifest, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("read dir %s: %w", dir, err)
	}
	var out []*Manifest
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		p := filepath.Join(dir, e.Name(), "module.yaml")
		info, err := os.Stat(p)
		if errors.Is(err, fs.ErrNotExist) {
			continue
		}
		if err != nil {
			return nil, fmt.Errorf("stat %s: %w", p, err)
		}
		if info.IsDir() {
			continue
		}
		m, err := LoadFile(p)
		if err != nil {
			return nil, err
		}
		out = append(out, m)
	}
	return out, nil
}

// Validate enforces required fields and basic structural invariants.
// It does NOT validate configs against JSON schemas or regex compilability
// (callers do that after load, since it's policy-specific).
func (m *Manifest) Validate() error {
	if m.ID == "" {
		return errors.New("id is required")
	}
	if m.Name == "" {
		return errors.New("name is required")
	}
	if m.Version == "" {
		return errors.New("version is required")
	}
	if len(m.SupportedModes) == 0 {
		return errors.New("supported_modes must list at least one mode")
	}
	for _, mode := range m.SupportedModes {
		switch mode {
		case "docker":
		case "host":
			// Declared in the manifest schema but the agent has no host
			// runtime — accepting it here would advertise a capability
			// that silently doesn't work. Reject loudly until host mode
			// is actually implemented.
			return fmt.Errorf("run mode %q is not implemented (only %q is supported); remove it from supported_modes", mode, "docker")
		default:
			return fmt.Errorf("unknown run mode %q (valid: docker)", mode)
		}
	}
	if m.HasMode("docker") && (m.Runtime.Docker == nil || m.Runtime.Docker.Image == "") {
		return errors.New("supported_modes includes docker but runtime.docker.image is missing")
	}
	if m.HasMode("host") && m.Runtime.Host == nil {
		return errors.New("supported_modes includes host but runtime.host is missing")
	}
	if len(m.Ports) == 0 {
		return errors.New("at least one port must be declared")
	}
	seen := map[string]bool{}
	for _, p := range m.Ports {
		if p.Name == "" {
			return errors.New("port.name is required")
		}
		if seen[p.Name] {
			return fmt.Errorf("duplicate port name %q", p.Name)
		}
		seen[p.Name] = true
		switch p.Proto {
		case "tcp", "udp":
		default:
			return fmt.Errorf("port %q: proto must be tcp or udp (got %q)", p.Name, p.Proto)
		}
		if p.Default == 0 {
			return fmt.Errorf("port %q: default is required", p.Name)
		}
	}
	if m.RCON != nil {
		if m.RCON.Adapter == "" {
			return errors.New("rcon.adapter is required when rcon is set")
		}
		if m.RCON.HostPort != "" && m.Port(m.RCON.HostPort) == nil {
			return fmt.Errorf("rcon.host_port %q does not match any declared port", m.RCON.HostPort)
		}
	}
	if len(m.UpdateProviders) == 0 {
		return errors.New("at least one update_provider is required")
	}
	providerIDs := map[string]bool{}
	for _, p := range m.UpdateProviders {
		if p.ID == "" {
			return errors.New("update_provider.id is required")
		}
		if providerIDs[p.ID] {
			return fmt.Errorf("duplicate update_provider id %q", p.ID)
		}
		providerIDs[p.ID] = true
		switch p.Kind {
		case "steamcmd":
			if p.AppID == "" {
				return fmt.Errorf("update_provider %q: app_id required for steamcmd kind", p.ID)
			}
		case "github":
			if p.Repo == "" {
				return fmt.Errorf("update_provider %q: repo required for github kind", p.ID)
			}
		case "direct":
			if p.URL == "" {
				return fmt.Errorf("update_provider %q: url required for direct kind", p.ID)
			}
		default:
			return fmt.Errorf("update_provider %q: unknown kind %q", p.ID, p.Kind)
		}
	}
	return nil
}

