package model

import (
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/go-errors/errors"

	"github.com/google/uuid"
	"github.com/oapi-codegen/nullable"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/validate"
	"github.com/phishingclub/phishingclub/vo"
)

// AllowDeny is a model for allow deny listing
type AllowDeny struct {
	ID              nullable.Nullable[uuid.UUID]     `json:"id"`
	CreatedAt       *time.Time                       `json:"createdAt"`
	UpdatedAt       *time.Time                       `json:"updatedAt"`
	Name            nullable.Nullable[vo.String127]  `json:"name"`
	Cidrs           nullable.Nullable[vo.IPNetSlice] `json:"cidrs"`
	JA4Fingerprints nullable.Nullable[string]        `json:"ja4Fingerprints"`
	CountryCodes    nullable.Nullable[string]        `json:"countryCodes"`
	Allowed         nullable.Nullable[bool]          `json:"allowed"`
	CompanyID       nullable.Nullable[uuid.UUID]     `json:"companyID"`
}

// Validate checks if the allow deny list has a valid state
func (r *AllowDeny) Validate() error {
	if err := validate.NullableFieldRequired("name", r.Name); err != nil {
		return err
	}
	if err := validate.NullableFieldRequired("filter type", r.Allowed); err != nil {
		return err
	}

	// at least one of cidrs, ja4 fingerprints, or country codes must be provided
	hasCidrs := false
	if r.Cidrs.IsSpecified() {
		if cidrs, err := r.Cidrs.Get(); err == nil && len(cidrs) > 0 {
			hasCidrs = true
		}
	}

	hasJA4 := false
	if r.JA4Fingerprints.IsSpecified() {
		if ja4, err := r.JA4Fingerprints.Get(); err == nil && ja4 != "" {
			hasJA4 = true
		}
	}

	hasCountryCodes := false
	if r.CountryCodes.IsSpecified() {
		if codes, err := r.CountryCodes.Get(); err == nil && codes != "" {
			hasCountryCodes = true
		}
	}

	if !hasCidrs && !hasJA4 && !hasCountryCodes {
		return errs.NewValidationError(
			errors.New("at least one of CIDRs, JA4 fingerprints, or country codes must be provided"),
		)
	}

	return nil
}

// ToDBMap converts the fields that can be stored or updated to a map
// if the value is nullable and not set, it is not included
// if the value is nullable and set, it is included, if it is null, it is set to nil
func (r *AllowDeny) ToDBMap() map[string]any {
	m := map[string]any{}
	if r.Name.IsSpecified() {
		m["name"] = nil
		if name, err := r.Name.Get(); err == nil {
			m["name"] = name.String()
		}
	}
	if r.Cidrs.IsSpecified() {
		m["cidrs"] = nil
		if cidrs, err := r.Cidrs.Get(); err == nil {
			cidrsStr := ""
			cidrsLen := len(cidrs)
			for i, cidr := range cidrs {
				if i == cidrsLen {
					cidrsStr += fmt.Sprintf("%s", cidr.String())

				} else {
					cidrsStr += fmt.Sprintf("%s\n", cidr.String())
				}
			}
			m["cidrs"] = cidrsStr
		}
	}
	if r.JA4Fingerprints.IsSpecified() {
		m["ja4_fingerprints"] = ""
		if ja4, err := r.JA4Fingerprints.Get(); err == nil {
			m["ja4_fingerprints"] = ja4
		}
	}
	if r.CountryCodes.IsSpecified() {
		m["country_codes"] = ""
		if codes, err := r.CountryCodes.Get(); err == nil {
			m["country_codes"] = codes
		}
	}
	if r.Allowed.IsSpecified() {
		m["allowed"] = nil
		if allowed, err := r.Allowed.Get(); err == nil {
			m["allowed"] = allowed
		}
	}
	if r.CompanyID.IsSpecified() {
		if r.CompanyID.IsNull() {
			m["company_id"] = nil
		} else {
			m["company_id"] = r.CompanyID.MustGet()
		}
	}

	return m
}

func (r *AllowDeny) IsIPAllowed(ip string) (bool, error) {
	isTypeAllowList := r.Allowed.MustGet()

	// if no cidrs configured, skip ip check (always pass)
	cidrs, err := r.Cidrs.Get()
	if err != nil || len(cidrs) == 0 {
		return true, nil
	}

	netIP := net.ParseIP(ip)
	if netIP == nil {
		return false, fmt.Errorf("invalid ip address: %s", ip)
	}

	for _, cidr := range cidrs {
		isInRange := cidr.Contains(netIP)
		// if allow list and ip is within range
		if isTypeAllowList && isInRange {
			return true, nil
		}
		// if deny list and ip is within range
		if !isTypeAllowList && isInRange {
			return false, nil
		}
	}

	// If this is an allow list and we didn't find the IP, it's not allowed
	if isTypeAllowList {
		return false, nil
	}

	// If this is a deny list and we didn't find the IP, it is allowed
	return true, nil
}

// IsJA4Allowed checks if a JA4 fingerprint is allowed based on the filter rules
func (r *AllowDeny) IsJA4Allowed(ja4 string) (bool, error) {
	if ja4 == "" {
		// if no ja4 fingerprint available, skip ja4 check
		return true, nil
	}

	isTypeAllowList := r.Allowed.MustGet()

	// get ja4 fingerprints list
	ja4FingerprintsStr, err := r.JA4Fingerprints.Get()
	if err != nil || ja4FingerprintsStr == "" {
		// if no ja4 fingerprints configured, skip ja4 check
		return true, nil
	}

	// parse fingerprints (newline separated)
	fingerprints := parseFingerprints(ja4FingerprintsStr)

	// check if ja4 matches any fingerprint (supports wildcard patterns with *)
	isMatch := false
	for _, fp := range fingerprints {
		if matchJA4Pattern(fp, ja4) {
			isMatch = true
			break
		}
	}

	// if allow list and ja4 matches
	if isTypeAllowList && isMatch {
		return true, nil
	}
	// if deny list and ja4 matches
	if !isTypeAllowList && isMatch {
		return false, nil
	}

	// If this is an allow list and ja4 didn't match, not allowed
	if isTypeAllowList {
		return false, nil
	}

	// If this is a deny list and ja4 didn't match, it is allowed
	return true, nil
}

// matchJA4Pattern checks if a JA4 fingerprint matches a pattern with wildcard support
// supports * as wildcard to match any characters
// examples:
//
//	t13d151*h2_8daaf6152771_* matches any fingerprint with that prefix and cipher hash
//	t13d*_*_* matches any TLS 1.3 fingerprint with SNI
//	* matches everything
func matchJA4Pattern(pattern, ja4 string) bool {
	// exact match (no wildcards)
	if pattern == ja4 {
		return true
	}

	// if pattern doesn't contain *, no match
	if !strings.Contains(pattern, "*") {
		return false
	}

	// wildcard-only pattern matches everything
	if pattern == "*" {
		return true
	}

	// split pattern by * and check each part exists in order
	parts := strings.Split(pattern, "*")
	pos := 0

	for i, part := range parts {
		if part == "" {
			continue
		}

		// find the part in the remaining string
		idx := strings.Index(ja4[pos:], part)
		if idx == -1 {
			return false
		}

		// for first part, must match at beginning
		if i == 0 && idx != 0 {
			return false
		}

		pos += idx + len(part)
	}

	// for last part, must match at end
	lastPart := parts[len(parts)-1]
	if lastPart != "" && !strings.HasSuffix(ja4, lastPart) {
		return false
	}

	return true
}

// parseFingerprints splits newline-separated fingerprints and trims whitespace
func parseFingerprints(input string) []string {
	var result []string
	lines := splitLines(input)
	for _, line := range lines {
		trimmed := trimSpace(line)
		if trimmed != "" {
			result = append(result, trimmed)
		}
	}
	return result
}

// splitLines splits a string by newlines
func splitLines(s string) []string {
	var lines []string
	start := 0
	for i := 0; i < len(s); i++ {
		if s[i] == '\n' {
			lines = append(lines, s[start:i])
			start = i + 1
		}
	}
	if start < len(s) {
		lines = append(lines, s[start:])
	}
	return lines
}

// trimSpace removes leading and trailing whitespace
func trimSpace(s string) string {
	start := 0
	end := len(s)

	for start < end && isSpace(s[start]) {
		start++
	}
	for end > start && isSpace(s[end-1]) {
		end--
	}

	return s[start:end]
}

// isSpace checks if a byte is whitespace
func isSpace(b byte) bool {
	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}

// IsCountryAllowed checks if a country code is allowed based on the filter rules
func (r *AllowDeny) IsCountryAllowed(countryCode string) bool {
	if countryCode == "" {
		// if no country code available, skip country check
		return true
	}

	isTypeAllowList := r.Allowed.MustGet()

	// get country codes list
	countryCodesStr, err := r.CountryCodes.Get()
	if err != nil || countryCodesStr == "" {
		// if no country codes configured, skip country check
		return true
	}

	// if country code is empty but we have country filters configured
	if countryCode == "" {
		// in allow list mode: unknown country should be denied
		// in deny list mode: unknown country should be allowed
		if isTypeAllowList {
			return false
		}
		return true
	}

	// parse country codes (newline separated)
	codes := parseCountryCodes(countryCodesStr)

	// check if country code matches any in the list (case-insensitive)
	isMatch := false
	countryCodeUpper := strings.ToUpper(countryCode)
	for _, code := range codes {
		if strings.ToUpper(code) == countryCodeUpper {
			isMatch = true
			break
		}
	}

	// if allow list and country matches
	if isTypeAllowList && isMatch {
		return true
	}
	// if deny list and country matches
	if !isTypeAllowList && isMatch {
		return false
	}

	// If this is an allow list and country didn't match, not allowed
	if isTypeAllowList {
		return false
	}

	// If this is a deny list and country didn't match, it is allowed
	return true
}

// parseCountryCodes splits newline-separated country codes and trims whitespace
func parseCountryCodes(input string) []string {
	var result []string
	lines := splitLines(input)
	for _, line := range lines {
		trimmed := trimSpace(line)
		if trimmed != "" {
			result = append(result, trimmed)
		}
	}
	return result
}
