package helpers

import (
	"context"
	"encoding/base64"
	"strings"

	"github.com/CycloneDX/cyclonedx-go"

	"github.com/anchore/syft/internal/licenses"
	"github.com/anchore/syft/internal/spdxlicense"
	"github.com/anchore/syft/syft/pkg"
)

// This should be a function that just surfaces licenses already validated in the package struct
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
	spdx, other, ex := separateLicenses(p)
	out := spdx
	out = append(out, other...)

	if len(other) > 0 || len(spdx) > 0 {
		// found non spdx related licenses
		// build individual license choices for each
		// complex expressions are not combined and set as NAME fields
		for _, e := range ex {
			if e == "" {
				continue
			}
			out = append(out, cyclonedx.LicenseChoice{
				License: &cyclonedx.License{
					Name: e,
				},
			})
		}
	} else if len(ex) > 0 {
		// only expressions found
		e := mergeSPDX(ex)
		if e != "" {
			out = append(out, cyclonedx.LicenseChoice{
				Expression: e,
			})
		}
	}

	if len(out) > 0 {
		return &out
	}

	return nil
}

func decodeLicenses(c *cyclonedx.Component) []pkg.License {
	licenses := make([]pkg.License, 0)
	if c == nil || c.Licenses == nil {
		return licenses
	}

	for _, l := range *c.Licenses {
		// these fields are mutually exclusive in the spec
		switch {
		case l.License != nil && l.License.ID != "":
			licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL))
		case l.License != nil && l.License.Name != "":
			licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL))
		case l.License != nil && l.License.URL != "":
			// Try to enrich license from URL when ID and Name are empty
			licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), "", l.License.URL))
		case l.Expression != "":
			licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression))
		default:
		}
	}

	return licenses
}

func separateLicenses(p pkg.Package) (spdx, other cyclonedx.Licenses, expressions []string) {
	ex := make([]string, 0)
	spdxc := cyclonedx.Licenses{}
	otherc := cyclonedx.Licenses{}
	/*
			pkg.License can be a couple of things: see above declarations
			- Complex SPDX expression
			- Some other Valid license ID
			- Some non-standard non spdx license

			To determine if an expression is a singular ID we first run it against the SPDX license list.

		The weird case we run into is if there is a package with a license that is not a valid SPDX expression
			and a license that is a valid complex expression. In this case we will surface the valid complex expression
			as a license choice and the invalid expression as a license string.

	*/
	seen := make(map[string]bool)
	for _, l := range p.Licenses.ToSlice() {
		// singular expression case
		// only ID field here since we guarantee that the license is valid
		if value, exists := spdxlicense.ID(l.SPDXExpression); exists {
			if len(l.URLs) > 0 {
				processLicenseURLs(l, value, &spdxc)
				continue
			}

			if _, exists := seen[value]; exists {
				continue
			}
			// try making set of license choices to avoid duplicates
			// only update if the license has more information
			spdxc = append(spdxc, cyclonedx.LicenseChoice{
				License: &cyclonedx.License{
					ID: value,
				},
			})
			seen[value] = true
			// we have added the license to the SPDX license list check next license
			continue
		}

		if l.SPDXExpression != "" && !strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) {
			// COMPLEX EXPRESSION CASE
			ex = append(ex, l.SPDXExpression)
			continue
		}

		// license string that are not valid spdx expressions or ids
		// we only use license Name here since we cannot guarantee that the license is a valid SPDX expression
		if len(l.URLs) > 0 && !strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) {
			processLicenseURLs(l, "", &otherc)
			continue
		}

		otherc = append(otherc, processCustomLicense(l)...)
	}
	return spdxc, otherc, ex
}

func processCustomLicense(l pkg.License) cyclonedx.Licenses {
	result := cyclonedx.Licenses{}
	if strings.HasPrefix(l.SPDXExpression, licenses.UnknownLicensePrefix) {
		cyclonedxLicense := &cyclonedx.License{
			Name: l.SPDXExpression,
		}
		if len(l.URLs) > 0 {
			cyclonedxLicense.URL = l.URLs[0]
		}
		if len(l.Contents) > 0 {
			cyclonedxLicense.Text = &cyclonedx.AttachedText{
				Content: base64.StdEncoding.EncodeToString([]byte(l.Contents)),
			}
			cyclonedxLicense.Text.ContentType = "text/plain"
			cyclonedxLicense.Text.Encoding = "base64"
		}
		result = append(result, cyclonedx.LicenseChoice{
			License: cyclonedxLicense,
		})
	} else {
		result = append(result, cyclonedx.LicenseChoice{
			License: &cyclonedx.License{
				Name: l.Value,
			},
		})
	}
	return result
}

func processLicenseURLs(l pkg.License, spdxID string, populate *cyclonedx.Licenses) {
	for _, url := range l.URLs {
		if spdxID == "" {
			// CycloneDX requires either an id or name to be present for a license
			// If l.Value is empty, use the URL as the name to ensure schema compliance
			// at this point we've already tried to enrich the license we just don't want the format
			// conversion to be lossy here
			name := l.Value
			if name == "" {
				name = url
			}
			*populate = append(*populate, cyclonedx.LicenseChoice{
				License: &cyclonedx.License{
					URL:  url,
					Name: name,
				},
			})
		} else {
			*populate = append(*populate, cyclonedx.LicenseChoice{
				License: &cyclonedx.License{
					ID:  spdxID,
					URL: url,
				},
			})
		}
	}
}

func mergeSPDX(ex []string) string {
	var candidate []string
	for _, e := range ex {
		// if the expression does not have balanced parens add them
		if !strings.HasPrefix(e, "(") && !strings.HasSuffix(e, ")") {
			e = "(" + e + ")"
		}
		candidate = append(candidate, e)
	}

	if len(candidate) == 1 {
		return reduceOuter(candidate[0])
	}

	return reduceOuter(strings.Join(candidate, " AND "))
}

func reduceOuter(expression string) string {
	expression = strings.TrimSpace(expression)

	// If the entire expression is wrapped in parentheses, check if they are redundant.
	if strings.HasPrefix(expression, "(") && strings.HasSuffix(expression, ")") {
		trimmed := expression[1 : len(expression)-1]
		if isBalanced(trimmed) {
			return reduceOuter(trimmed) // Recursively reduce the trimmed expression.
		}
	}

	return expression
}

func isBalanced(expression string) bool {
	count := 0
	for _, c := range expression {
		switch c {
		case '(':
			count++
		case ')':
			count--
			if count < 0 {
				return false
			}
		default:
		}
	}
	return count == 0
}
