// Package linkcheck implements a helper library to search for links
// in a message text and validate them by trying to call them
package linkcheck

import (
	"regexp"
	"strings"
	"sync"

	"github.com/Luzifer/go_helpers/v2/str"
)

type (
	// Checker contains logic to detect and resolve links in a message
	Checker struct {
		res *resolver
	}
)

// New creates a new Checker instance with default settings
func New(opts ...func(*Checker)) *Checker {
	c := &Checker{
		res: defaultResolver,
	}

	for _, o := range opts {
		o(c)
	}

	return c
}

func withResolver(r *resolver) func(*Checker) {
	return func(c *Checker) { c.res = r }
}

// HeuristicScanForLinks takes a message and tries to find links
// within that message. Common methods like putting spaces into links
// are tried to circumvent.
func (c Checker) HeuristicScanForLinks(message string) []string {
	return c.scan(message,
		c.scanPlainNoObfuscate,
		c.scanDotObfuscation,
		c.scanObfuscateSpace,
		c.scanObfuscateSpecialCharsAndSpaces(regexp.MustCompile(`[^a-zA-Z0-9.:/\s_-]`), ""), // Leave dots intact and just join parts
		c.scanObfuscateSpecialCharsAndSpaces(regexp.MustCompile(`[^a-zA-Z0-9:/\s_-]`), "."), // Remove dots also and connect by them
	)
}

// ScanForLinks takes a message and tries to find links within that
// message. This only detects links without any means of obfuscation
// like putting spaces into the link.
func (c Checker) ScanForLinks(message string) (links []string) {
	return c.scan(message, c.scanPlainNoObfuscate)
}

func (Checker) scan(message string, scanFns ...func(string) []string) (links []string) {
	for _, scanner := range scanFns {
		if links = scanner(message); links != nil {
			return links
		}
	}

	return links
}

func (c Checker) scanDotObfuscation(message string) (links []string) {
	message = regexp.MustCompile(`(?i)\s*\(?dot\)?\s*`).ReplaceAllString(message, ".")
	return c.scanPlainNoObfuscate(message)
}

func (c Checker) scanObfuscateSpace(message string) (links []string) {
	// Spammers use spaces in their links to prevent link protection matches
	parts := regexp.MustCompile(`\s+`).Split(message, -1)
	return c.scanPartsConnected(parts, "")
}

func (c Checker) scanObfuscateSpecialCharsAndSpaces(set *regexp.Regexp, connector string) func(string) []string {
	return func(message string) (links []string) {
		// First clean URL from all characters not acceptable in Domains (plus some extra chars)
		message = set.ReplaceAllString(message, " ")
		parts := regexp.MustCompile(`\s+`).Split(message, -1)
		return c.scanPartsConnected(parts, connector)
	}
}

func (c Checker) scanPartsConnected(parts []string, connector string) (links []string) {
	wg := new(sync.WaitGroup)

	for ptJoin := 2; ptJoin < len(parts); ptJoin++ {
		for i := 0; i <= len(parts)-ptJoin; i++ {
			c.res.Resolve(resolverQueueEntry{
				Link:      strings.Join(parts[i:i+ptJoin], connector),
				Callback:  func(link string) { links = str.AppendIfMissing(links, link) },
				WaitGroup: wg,
			})
		}
	}

	wg.Wait()

	return links
}

func (c Checker) scanPlainNoObfuscate(message string) (links []string) {
	var (
		parts = regexp.MustCompile(`\s+`).Split(message, -1)
		wg    = new(sync.WaitGroup)
	)

	for _, part := range parts {
		c.res.Resolve(resolverQueueEntry{
			Link:      part,
			Callback:  func(link string) { links = str.AppendIfMissing(links, link) },
			WaitGroup: wg,
		})
	}

	wg.Wait()

	return links
}