package routes

import (
	"context"
	"fmt"
	"regexp"
	"sort"
	"strings"

	"gorm.io/gorm"
)

const defaultProductRule = "/product/{rewrite}"
const defaultCategoryRule = "/{id}-{rewrite}"
const optionalLanguagePrefix = "(?:/[a-zA-Z]{2}(?:-[a-zA-Z]{2})?)?"

type Service struct {
	db     *gorm.DB
	prefix string
}

type ProductRoute struct {
	Rule   string
	Prefix string
	regex  *regexp.Regexp
}

type ProductMatch struct {
	ID   int64
	Slug string
}

type ProductURLData struct {
	ID                 int64
	Slug               string
	CategoryPath       string
	ProductAttributeID int64
	EAN13              string
	LanguagePrefix     string
}

type CategoryRoute struct {
	Rule   string
	Prefix string
	regex  *regexp.Regexp
}

type CategoryMatch struct {
	ID   int64
	Slug string
}

type CategoryURLData struct {
	ID             int64
	Slug           string
	LanguagePrefix string
}

type RouteMatcher interface {
	Owns(path string) bool
}

type combinedMatcher struct {
	matchers []RouteMatcher
}

var fallbackProductSegment = regexp.MustCompile(`^(?P<id>\d+)(?:-\d+)?-(?P<rewrite>.+?)(?:-[^-/.]*)?\.html$`)
var fallbackCategorySegment = regexp.MustCompile(`^(?P<id>\d+)-(?P<rewrite>[^/]+)$`)

func NewService(db *gorm.DB, prefix string) *Service {
	return &Service{db: db, prefix: prefix}
}

func CombineMatchers(matchers ...RouteMatcher) RouteMatcher {
	return combinedMatcher{matchers: matchers}
}

func (s *Service) LoadProductRoute(ctx context.Context) (*ProductRoute, error) {
	rule, err := s.loadRouteRule(ctx, "PS_ROUTE_product_rule", defaultProductRule)
	if err != nil {
		return nil, err
	}
	return CompileProductRoute(rule)
}

func (s *Service) LoadCategoryRoute(ctx context.Context) (*CategoryRoute, error) {
	rule, err := s.loadRouteRule(ctx, "PS_ROUTE_category_rule", defaultCategoryRule)
	if err != nil {
		return nil, err
	}
	return CompileCategoryRoute(rule)
}

func (s *Service) loadRouteRule(ctx context.Context, key, fallback string) (string, error) {
	rule := fallback
	if s != nil && s.db != nil {
		var row struct {
			Value string `gorm:"column:value"`
		}
		query := fmt.Sprintf("SELECT value FROM %sconfiguration WHERE name = ? LIMIT 1", s.prefix)
		if err := s.db.WithContext(ctx).Raw(query, key).Scan(&row).Error; err != nil {
			return "", err
		}
		if strings.TrimSpace(row.Value) != "" {
			rule = row.Value
		}
	}
	return rule, nil
}

func CompileCategoryRoute(rule string) (*CategoryRoute, error) {
	compiled, prefix, err := compileRouteRule(rule, defaultCategoryRule)
	if err != nil {
		return nil, err
	}
	return &CategoryRoute{
		Rule:   normalizeRule(rule, defaultCategoryRule),
		Prefix: prefix,
		regex:  compiled,
	}, nil
}

func (r *CategoryRoute) Match(path string) (slug string, ok bool) {
	match, ok := r.MatchInfo(path)
	if !ok {
		return "", false
	}
	return match.Slug, true
}

func (r *CategoryRoute) MatchInfo(path string) (*CategoryMatch, bool) {
	if r == nil || r.regex == nil {
		return fallbackCategoryMatch(path)
	}
	matches := r.regex.FindStringSubmatch(path)
	if matches != nil {
		out := &CategoryMatch{}
		for idx, name := range r.regex.SubexpNames() {
			if idx >= len(matches) || matches[idx] == "" {
				continue
			}
			switch name {
			case "rewrite":
				out.Slug = matches[idx]
			case "id", "id_category":
				out.ID = parseInt64(matches[idx])
			}
		}
		if out.ID != 0 {
			return out, true
		}
	}
	return fallbackCategoryMatch(path)
}

func (r *CategoryRoute) Owns(path string) bool {
	match, ok := r.MatchInfo(path)
	return ok && match.ID != 0
}

func (r *CategoryRoute) BuildPath(data CategoryURLData) string {
	rule := defaultCategoryRule
	if r != nil {
		rule = normalizeRule(r.Rule, defaultCategoryRule)
	}

	var path strings.Builder
	path.Grow(len(rule) + len(data.Slug) + 16)
	path.WriteString(normalizeLanguagePrefix(data.LanguagePrefix))
	for i := 0; i < len(rule); {
		if rule[i] != '{' {
			path.WriteByte(rule[i])
			i++
			continue
		}

		end := strings.IndexByte(rule[i:], '}')
		if end < 0 {
			break
		}
		end += i
		token := rule[i+1 : end]
		name, before, after := parseToken(token)
		value := categoryTokenValue(name, data)
		if value == "" {
			i = end + 1
			continue
		}
		path.WriteString(before)
		path.WriteString(value)
		path.WriteString(after)
		i = end + 1
	}

	result := path.String()
	if result == "" {
		return "/"
	}
	if !strings.HasPrefix(result, "/") {
		return "/" + result
	}
	return result
}

func (m combinedMatcher) Owns(path string) bool {
	for _, matcher := range m.matchers {
		if matcher != nil && matcher.Owns(path) {
			return true
		}
	}
	return false
}

func CompileProductRoute(rule string) (*ProductRoute, error) {
	compiled, prefix, err := compileRouteRule(rule, defaultProductRule)
	if err != nil {
		return nil, err
	}
	return &ProductRoute{
		Rule:   normalizeRule(rule, defaultProductRule),
		Prefix: prefix,
		regex:  compiled,
	}, nil
}

func compileRouteRule(rule, fallback string) (*regexp.Regexp, string, error) {
	rule = normalizeRule(rule, fallback)
	var pattern strings.Builder
	pattern.WriteString("^")
	pattern.WriteString(optionalLanguagePrefix)
	for i := 0; i < len(rule); {
		if rule[i] != '{' {
			pattern.WriteString(regexp.QuoteMeta(string(rule[i])))
			i++
			continue
		}

		end := strings.IndexByte(rule[i:], '}')
		if end < 0 {
			return nil, "", fmt.Errorf("invalid product route rule %q", rule)
		}
		end += i
		token := rule[i+1 : end]
		pattern.WriteString(tokenRegex(token))
		i = end + 1
	}
	pattern.WriteString("$")

	compiled, err := regexp.Compile(pattern.String())
	if err != nil {
		return nil, "", fmt.Errorf("compile route rule %q: %w", rule, err)
	}
	return compiled, staticPrefix(rule), nil
}

func (r *ProductRoute) Match(path string) (slug string, ok bool) {
	match, ok := r.MatchInfo(path)
	if !ok {
		return "", false
	}
	return match.Slug, true
}

func (r *ProductRoute) MatchInfo(path string) (*ProductMatch, bool) {
	if r == nil || r.regex == nil {
		return fallbackProductMatch(path)
	}
	matches := r.regex.FindStringSubmatch(path)
	if matches != nil {
		out := &ProductMatch{}
		for idx, name := range r.regex.SubexpNames() {
			if idx >= len(matches) || matches[idx] == "" {
				continue
			}
			switch name {
			case "rewrite":
				out.Slug = matches[idx]
			case "id", "id_product":
				out.ID = parseInt64(matches[idx])
			}
		}
		if out.ID != 0 {
			return out, true
		}
	}
	return fallbackProductMatch(path)
}

func (r *ProductRoute) Owns(path string) bool {
	match, ok := r.MatchInfo(path)
	return ok && match.ID != 0
}

func (r *ProductRoute) BuildPath(data ProductURLData) string {
	rule := defaultProductRule
	if r != nil {
		rule = normalizeRule(r.Rule, defaultProductRule)
	}

	var path strings.Builder
	path.Grow(len(rule) + len(data.Slug) + len(data.CategoryPath) + len(data.EAN13) + 16)
	path.WriteString(normalizeLanguagePrefix(data.LanguagePrefix))
	for i := 0; i < len(rule); {
		if rule[i] != '{' {
			path.WriteByte(rule[i])
			i++
			continue
		}

		end := strings.IndexByte(rule[i:], '}')
		if end < 0 {
			break
		}
		end += i
		token := rule[i+1 : end]
		name, before, after := parseToken(token)
		value := productTokenValue(name, data)
		if value == "" {
			i = end + 1
			continue
		}
		path.WriteString(before)
		path.WriteString(value)
		path.WriteString(after)
		i = end + 1
	}

	result := path.String()
	if result == "" {
		return "/"
	}
	if !strings.HasPrefix(result, "/") {
		return "/" + result
	}
	return result
}

func normalizeRule(rule, fallback string) string {
	rule = strings.TrimSpace(rule)
	if rule == "" {
		rule = fallback
	}
	if !strings.HasPrefix(rule, "/") {
		rule = "/" + rule
	}
	return rule
}

func staticPrefix(rule string) string {
	rule = strings.TrimSpace(rule)
	if rule == "" {
		return "/"
	}
	if !strings.HasPrefix(rule, "/") {
		rule = "/" + rule
	}
	if idx := strings.IndexByte(rule, '{'); idx >= 0 {
		prefix := rule[:idx]
		if prefix == "" {
			return "/"
		}
		return prefix
	}
	return rule
}

func tokenRegex(token string) string {
	name, before, after := parseToken(token)
	if name == "category" && after == "/" {
		return "(?:[^/]+/)+"
	}
	if name == "ean13" {
		pattern := regexp.QuoteMeta(before) + "[^/]*" + regexp.QuoteMeta(after)
		return "(?:" + pattern + ")?"
	}
	pattern := tokenPattern(name)
	fragment := regexp.QuoteMeta(before) + pattern + regexp.QuoteMeta(after)
	if name != "rewrite" && strings.Contains(token, ":") {
		return "(?:" + fragment + ")?"
	}
	return fragment
}

func parseToken(token string) (name, before, after string) {
	known := []string{
		"id_product_attribute",
		"id_product",
		"id_category",
		"id_manufacturer",
		"id_supplier",
		"id_shop",
		"id_lang",
		"categories",
		"category",
		"rewrite",
		"ean13",
		"reference",
		"meta_title",
		"manufacturer",
		"supplier",
		"price",
		"id",
	}
	sort.SliceStable(known, func(i, j int) bool {
		return len(known[i]) > len(known[j])
	})
	for _, candidate := range known {
		if idx := strings.Index(token, candidate); idx >= 0 {
			before = trimTokenAffix(token[:idx])
			after = trimTokenAffix(token[idx+len(candidate):])
			return candidate, before, after
		}
	}
	return strings.Trim(token, ":"), "", ""
}

func trimTokenAffix(value string) string {
	return strings.Trim(value, ":")
}

func tokenPattern(name string) string {
	switch name {
	case "rewrite":
		return "(?P<rewrite>[^/]+)"
	case "category", "manufacturer", "supplier", "reference", "meta_title":
		return "[^/]+"
	case "categories":
		return "(?:.+?/)?"
	case "id", "id_product", "id_category", "id_manufacturer", "id_supplier", "id_shop", "id_lang", "id_product_attribute":
		return "[0-9]+"
	case "ean13", "price":
		return "[^/]+"
	default:
		return "[^/]+"
	}
}

func productTokenValue(name string, data ProductURLData) string {
	switch name {
	case "id", "id_product":
		if data.ID == 0 {
			return ""
		}
		return fmt.Sprintf("%d", data.ID)
	case "rewrite":
		return strings.Trim(data.Slug, "/")
	case "category", "categories":
		value := strings.Trim(data.CategoryPath, "/")
		if value == "" {
			return ""
		}
		return value
	case "id_product_attribute":
		if data.ProductAttributeID == 0 {
			return ""
		}
		return fmt.Sprintf("%d", data.ProductAttributeID)
	case "ean13":
		return strings.TrimSpace(data.EAN13)
	default:
		return ""
	}
}

func categoryTokenValue(name string, data CategoryURLData) string {
	switch name {
	case "id", "id_category":
		if data.ID == 0 {
			return ""
		}
		return fmt.Sprintf("%d", data.ID)
	case "rewrite":
		return strings.Trim(data.Slug, "/")
	default:
		return ""
	}
}

func normalizeLanguagePrefix(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	if !strings.HasPrefix(value, "/") {
		value = "/" + value
	}
	return strings.TrimRight(value, "/")
}

func fallbackProductSlug(path string) (string, bool) {
	match, ok := fallbackProductMatch(path)
	if !ok {
		return "", false
	}
	return match.Slug, true
}

func fallbackProductMatch(path string) (*ProductMatch, bool) {
	path = strings.TrimSpace(path)
	if path == "" {
		return nil, false
	}
	if hasExcludedContentSegment(path) {
		return nil, false
	}
	lastSlash := strings.LastIndex(path, "/")
	segment := path
	if lastSlash >= 0 {
		segment = path[lastSlash+1:]
	}
	matches := fallbackProductSegment.FindStringSubmatch(segment)
	if matches == nil {
		return nil, false
	}
	out := &ProductMatch{}
	for idx, name := range fallbackProductSegment.SubexpNames() {
		if idx >= len(matches) || matches[idx] == "" {
			continue
		}
		switch name {
		case "rewrite":
			out.Slug = matches[idx]
		case "id":
			out.ID = parseInt64(matches[idx])
		}
	}
	if out.ID == 0 {
		return nil, false
	}
	return out, true
}

func fallbackCategorySlug(path string) (string, bool) {
	match, ok := fallbackCategoryMatch(path)
	if !ok {
		return "", false
	}
	return match.Slug, true
}

func fallbackCategoryMatch(path string) (*CategoryMatch, bool) {
	path = strings.TrimSpace(path)
	if path == "" {
		return nil, false
	}
	if hasExcludedContentSegment(path) {
		return nil, false
	}
	lastSlash := strings.LastIndex(path, "/")
	segment := path
	if lastSlash >= 0 {
		segment = path[lastSlash+1:]
	}
	matches := fallbackCategorySegment.FindStringSubmatch(segment)
	if matches == nil {
		return nil, false
	}
	out := &CategoryMatch{}
	for idx, name := range fallbackCategorySegment.SubexpNames() {
		if idx >= len(matches) || matches[idx] == "" {
			continue
		}
		switch name {
		case "rewrite":
			out.Slug = matches[idx]
		case "id":
			out.ID = parseInt64(matches[idx])
		}
	}
	if out.ID == 0 {
		return nil, false
	}
	return out, true
}

func parseInt64(value string) int64 {
	var n int64
	for _, r := range value {
		if r < '0' || r > '9' {
			return 0
		}
		n = n*10 + int64(r-'0')
	}
	return n
}

func hasExcludedContentSegment(path string) bool {
	path = strings.Trim(path, "/")
	if path == "" {
		return false
	}
	segments := strings.Split(path, "/")
	start := 0
	if len(segments) > 0 && len(segments[0]) >= 2 && len(segments[0]) <= 5 {
		start = 1
	}
	for i := start; i < len(segments); i++ {
		if strings.EqualFold(strings.TrimSpace(segments[i]), "content") {
			return true
		}
	}
	return false
}
