package runtime

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"

	native_errors "github.com/haproxytech/client-native/v5/errors"
	"github.com/haproxytech/client-native/v5/models"
)

// ShowACLS returns Acl files description from runtime
func (s *SingleRuntime) ShowACLS() (models.ACLFiles, error) {
	response, err := s.ExecuteWithResponse("show acl")
	if err != nil {
		return nil, fmt.Errorf("%s %w", err.Error(), native_errors.ErrNotFound)
	}
	return s.parseACLS(response), nil
}

// parseACLS parses output from `show acl` command and return array of acl files
// First line in output represents format and is ignored
// Sample output format:
// # id (file) description
// -0 (/etc/acl/blocklist.txt) pattern loaded from file '/etc/acl/blocklist.txt' used by acl at file '/usr/local/etc/haproxy/haproxy.cfg' line 59
// -1 () acl 'src' file '/usr/local/etc/haproxy/haproxy.cfg' line 59
func (s *SingleRuntime) parseACLS(output string) models.ACLFiles {
	if output == "" {
		return nil
	}
	acls := models.ACLFiles{}

	lines := strings.Split(strings.TrimSpace(output), "\n")
	for _, line := range lines {
		m := s.parseACL(line)
		if m != nil {
			acls = append(acls, m)
		}
	}
	return acls
}

// parseACL parses one line from ACL files array and return it structured
func (s *SingleRuntime) parseACL(line string) *models.ACLFile {
	if line == "" || strings.HasPrefix(strings.TrimSpace(line), "# id") {
		return nil
	}

	parts := strings.Fields(line)
	if len(parts) < 3 {
		return nil
	}

	m := &models.ACLFile{
		ID:          parts[0],
		StorageName: findStorageName(parts[1], line),
		Description: strings.Join(parts[2:], " "),
	}

	return m
}

// findStorageName checks if acl name exists and extracts it
func findStorageName(name, line string) string {
	name = strings.TrimSuffix(strings.TrimPrefix(name, "("), ")")
	if name == "" {
		re := regexp.MustCompile(`acl\s'(.*)'\sfile`)
		matches := re.FindStringSubmatch(line)
		if matches != nil {
			name = matches[1]
		}
	}
	return name
}

// GetACL returns one structured runtime Acl file
func (s *SingleRuntime) GetACL(storageName string) (*models.ACLFile, error) {
	if storageName == "" {
		return nil, fmt.Errorf("%s %w", "Argument nameOrFile empty", native_errors.ErrGeneral)
	}
	acls, err := s.ShowACLS()
	if err != nil {
		return nil, err
	}

	for _, m := range acls {
		if m.StorageName == storageName || storageName == "#"+m.ID {
			return m, nil
		}
	}
	return nil, fmt.Errorf("%s %w", storageName, native_errors.ErrNotFound)
}

// ShowACLFileEntries returns one acl runtime entries
func (s *SingleRuntime) ShowACLFileEntries(storageName string) (models.ACLFilesEntries, error) {
	if storageName == "" {
		return nil, fmt.Errorf("%s %w", "Argument file empty", native_errors.ErrGeneral)
	}
	cmd := fmt.Sprintf("show acl %s", storageName)
	response, err := s.ExecuteWithResponse(cmd)
	if err != nil {
		return nil, fmt.Errorf("%s %w", err.Error(), native_errors.ErrNotFound)
	}
	return ParseACLFileEntries(response, true)
}

// ParseACLFileEntries parses array of entries in one Acl file
// One line sample entry:
// ID			  Value
// 0x560f3f9e8600 10.178.160.0
func ParseACLFileEntries(output string, hasID bool) (models.ACLFilesEntries, error) {
	if output == "" || strings.HasPrefix(strings.TrimSpace(output), "Unknown ACL identifier.") {
		return nil, native_errors.ErrNotFound
	}
	me := models.ACLFilesEntries{}

	lines := strings.Split(strings.TrimSpace(output), "\n")
	for _, line := range lines {
		e := parseACLFileEntry(line, hasID)
		if e != nil {
			me = append(me, e)
		}
	}
	return me, nil
}

// parseACLFileEntry parses one entry in one Acl file/runtime and returns it structured
func parseACLFileEntry(line string, hasID bool) *models.ACLFileEntry {
	if line == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
		return nil
	}

	parts := strings.Fields(line)
	if len(parts) < 2 {
		return nil
	}

	m := &models.ACLFileEntry{}
	if hasID {
		m.ID = parts[0] // acl entries from runtime have ID
		m.Value = parts[1]
	} else {
		m.Value = parts[0]
	}
	return m
}

// AddACLFileEntry adds an entry into the Acl file
func (s *SingleRuntime) AddACLFileEntry(aclID, value string) error {
	if aclID == "" || value == "" {
		return fmt.Errorf("%s %w", "One or more Arguments empty", native_errors.ErrGeneral)
	}
	m, _ := s.GetACLFileEntry(aclID, value)
	if m != nil {
		return fmt.Errorf("%w", native_errors.ErrAlreadyExists)
	}
	cmd := fmt.Sprintf("add acl #%s %s", aclID, value)
	response, err := s.ExecuteWithResponse(cmd)
	if err != nil {
		return fmt.Errorf("%s %w", err.Error(), native_errors.ErrGeneral)
	}
	if strings.Contains(response, "not") && strings.Contains(response, "valid") {
		return fmt.Errorf("%s %w", strings.TrimSpace(response), native_errors.ErrGeneral)
	}
	return nil
}

func (s *SingleRuntime) PrepareACL(aclID string) (version string, err error) {
	cmd := fmt.Sprintf("prepare acl %s", aclID)
	response, err := s.ExecuteWithResponse(cmd)
	if err != nil {
		return "", fmt.Errorf("%s %w", err.Error(), native_errors.ErrGeneral)
	}
	parts := strings.Split(response, ":")
	if len(parts) < 3 {
		return "", fmt.Errorf("%s %w", err.Error(), native_errors.ErrGeneral)
	}
	version = strings.TrimSpace(parts[2])
	if _, err = strconv.ParseInt(version, 10, 64); err == nil {
		return version, nil
	}
	return "", fmt.Errorf("%s %w", err.Error(), native_errors.ErrGeneral)
}

func (s *SingleRuntime) AddACLVersioned(version, aclID, value string) error {
	cmd := fmt.Sprintf("add acl @%s %s %s", version, aclID, value)
	if err := s.Execute(cmd); err != nil {
		return fmt.Errorf("%s %w", err.Error(), native_errors.ErrGeneral)
	}
	return nil
}

func (s *SingleRuntime) CommitACL(version, aclID string) error {
	cmd := fmt.Sprintf("commit acl @%s %s", version, aclID)
	if err := s.Execute(cmd); err != nil {
		return fmt.Errorf("%s %w", err.Error(), native_errors.ErrGeneral)
	}
	return nil
}

// GetACLFileEntry returns one Acl runtime setting
func (s *SingleRuntime) GetACLFileEntry(aclID, value string) (*models.ACLFileEntry, error) {
	cmd := fmt.Sprintf("get acl #%s %s", aclID, value)
	response, err := s.ExecuteWithResponse(cmd)
	if err != nil {
		return nil, fmt.Errorf("%s %w", err.Error(), native_errors.ErrNotFound)
	}

	matched := false
	m := &models.ACLFileEntry{}
	parts := strings.Split(response, ",")
	for _, p := range parts {
		kv := strings.Split(p, "=")
		switch key := strings.TrimSpace(kv[0]); {
		case key == "pattern":
			m.Value = strings.Trim(strings.TrimSpace(kv[1]), "\"")
		case key == "match":
			matched = true
		}
	}

	if m.Value == "" || !matched {
		return nil, fmt.Errorf("%s %w", value, native_errors.ErrNotFound)
	}
	return m, nil
}

// DeleteACLFileEntry deletes all the Acl entries from the Acl by its value
func (s *SingleRuntime) DeleteACLFileEntry(aclID, value string) error {
	if aclID == "" || value == "" {
		return fmt.Errorf("%s %w", "One or more Arguments empty", native_errors.ErrGeneral)
	}
	cmd := fmt.Sprintf("del acl #%s %s", aclID, value)
	response, err := s.ExecuteWithResponse(cmd)
	if err != nil {
		return fmt.Errorf("%s %w", err.Error(), native_errors.ErrNotFound)
	}
	if strings.Contains(response, "not") && strings.Contains(response, "found") {
		return fmt.Errorf("%s %w", strings.TrimSpace(response), native_errors.ErrGeneral)
	}
	return nil
}
