package service

import (
	"errors"
	"fmt"
	"net"
	"strings"
	"unicode"

	"github.com/tinyauthapp/tinyauth/internal/model"
	"github.com/tinyauthapp/tinyauth/internal/utils/logger"
	"go.uber.org/dig"
)

type LabelProvider interface {
	Lookup(locator func(name string, app *model.App) bool) error
}

type AccessControlsService struct {
	log           *logger.Logger
	config        *model.Config
	runtime       *model.RuntimeConfig
	labelProvider LabelProvider
}

type AccessControlServiceInput struct {
	dig.In

	Log           *logger.Logger
	Config        *model.Config
	Runtime       *model.RuntimeConfig
	LabelProvider LabelProvider `optional:"true"`
}

func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsService {

	return &AccessControlsService{
		log:           i.Log,
		config:        i.Config,
		runtime:       i.Runtime,
		labelProvider: i.LabelProvider,
	}
}

func (service *AccessControlsService) ensureAscii(str string) bool {
	for i := 0; i < len(str); i++ {
		if str[i] > unicode.MaxASCII {
			return false
		}
	}
	return true
}

func (service *AccessControlsService) normalizeDomain(domain string) string {
	if host, _, err := net.SplitHostPort(domain); err == nil {
		domain = host
	}
	domain = strings.TrimRight(domain, ".")
	return strings.ToLower(domain)
}

func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) {
	if !service.ensureAscii(domain) {
		return nil, errors.New("domain contains non-ascii characters")
	}

	normalizedDomain := service.normalizeDomain(domain)

	if !strings.HasSuffix(normalizedDomain, "."+service.runtime.CookieDomain) && normalizedDomain != service.runtime.CookieDomain {
		return nil, fmt.Errorf("domain does not match cookie domain, expected %s (or a subdomain), got %s", service.runtime.CookieDomain, domain)
	}

	var domainMatch *model.App
	var nameMatch *model.App
	var nameMatchedApps []string

	locatorFunc := func(name string, app *model.App) bool {
		if app.Config.Domain != "" {
			if !service.ensureAscii(app.Config.Domain) {
				service.log.App.Warn().Str("name", name).Str("domain", app.Config.Domain).Msg("Domain contains non-ascii characters, skipping")
				return false
			}
			if normalizedDomain == service.normalizeDomain(app.Config.Domain) {
				service.log.App.Debug().Str("name", name).Msg("Found matching container by domain")
				domainMatch = app
				return true
			}
			return false
		}
		if strings.HasPrefix(normalizedDomain, strings.ToLower(name+".")) {
			service.log.App.Debug().Str("name", name).Msg("Found matching container by app name")
			nameMatch = app
			nameMatchedApps = append(nameMatchedApps, name)
		}
		return false
	}

	err := lookup(locatorFunc)
	if err != nil {
		return nil, err
	}

	if domainMatch != nil {
		service.log.App.Debug().Str("domain", domain).Msg("Found matching app by domain")
		return domainMatch, nil
	}

	if nameMatch == nil {
		service.log.App.Debug().Str("domain", domain).Msg("No match found for domain, skipping")
		return nil, nil
	}

	if len(nameMatchedApps) > 1 {
		return nil, fmt.Errorf("domain matched multiple apps by name prefix, use explicit domain config")
	}

	service.log.App.Debug().Str("domain", domain).Msg("Found matching app by app name")
	return nameMatch, nil
}

func (service *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) {
	return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error {
		for app, config := range service.config.Apps {
			if ok := locator(app, &config); ok {
				return nil
			}
		}
		return nil
	})
}

func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
	// First check in the static config
	app, err := service.lookupStaticACLs(domain)

	// Will never return an error here, but we need to check it
	if err != nil {
		return nil, err
	}

	if app != nil {
		service.log.App.Debug().Msg("Using static ACLs for app")
		return app, nil
	}

	// If we have a label provider configured, try to get ACLs from it
	if service.labelProvider != nil {
		return service.getACLs(domain, service.labelProvider.Lookup)
	}

	// No labels
	return nil, nil
}
