// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package svcs

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/miekg/dns"

	"git.happydns.org/happyDomain/internal/helpers"
	svc "git.happydns.org/happyDomain/internal/serviceanalyzer"
	"git.happydns.org/happyDomain/model"
	"git.happydns.org/happyDomain/services/common"
)

type DMARC struct {
	Record *happydns.TXT `json:"txt"`
}

func (s *DMARC) GetNbResources() int {
	return 1
}

func (s *DMARC) GenComment() string {
	t := DMARCFields{}
	t.Analyze(s.Record.Txt)

	var b strings.Builder

	if t.ADKIM && t.ASPF {
		b.WriteString("strict ")
	} else if t.ADKIM {
		b.WriteString("SPF relaxed ")
	} else if t.ASPF {
		b.WriteString("DKIM relaxed ")
	} else {
		b.WriteString("relaxed ")
	}

	if t.Request != "" {
		b.WriteString(t.Request)
		b.WriteString(" ")
	}

	if t.Percent < 100 {
		b.WriteString(strconv.Itoa(int(t.Percent)))
		b.WriteString(" %")
	}

	return b.String()
}

func (t *DMARC) GetRecords(domain string, ttl uint32, origin string) (rrs []happydns.Record, e error) {
	return []happydns.Record{t.Record}, nil
}

type DMARCFields struct {
	Version           uint            `json:"version" happydomain:"label=Version,placeholder=1,required,description=The version of DMARC to use.,default=1,hidden"`
	Request           string          `json:"p" happydomain:"label=Requested Mail Receiver policy,choices=none;quarantine;reject,description=Indicates the policy to be enacted by the Receiver,required"`
	SRequest          string          `json:"sp" happydomain:"label=Requested Mail Receiver policy for all subdomains,choices=;none;quarantaine;reject,description=Indicates the policy to be enacted by the Receiver when it receives mail for a subdomain"`
	AURI              []string        `json:"rua" happydomain:"label=RUA,description=Addresses for aggregate feedback,placeholder=mailto:name@example.com"`
	FURI              []string        `json:"ruf" happydomain:"label=RUF,description=Addresses for message-specific failure information,placeholder=mailto:name@example.com"`
	ADKIM             bool            `json:"adkim" happydomain:"label=Strict DKIM Alignment"`
	ASPF              bool            `json:"aspf" happydomain:"label=Strict SPF Alignment"`
	AInterval         common.Duration `json:"ri" happydomain:"label=Interval between aggregate reports"`
	FailureOptions    []string        `json:"fo" happydomain:"label=Failure reporting options,choices=0;1;d;s"`
	RegisteredFormats []string        `json:"rf" happydomain:"label=Format of the failure reports,choices=;afrf"`
	Percent           uint8           `json:"pct" happydomain:"label=Policy applies on,description=Percentage of messages to which the DMARC policy is to be applied.,unit=%"`
}

func analyseFields(txt string) map[string]string {
	ret := map[string]string{}

	for _, f := range strings.Split(txt, ";") {
		f = strings.TrimSpace(f)

		kv := strings.SplitN(f, "=", 2)
		if len(kv) == 1 {
			ret[strings.TrimSpace(kv[0])] = ""
		} else {
			ret[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
		}
	}

	return ret
}

func (t *DMARCFields) Analyze(txt string) error {
	fields := analyseFields(txt)

	if v, ok := fields["v"]; ok {
		if !strings.HasPrefix(v, "DMARC") {
			return fmt.Errorf("not a valid DMARC record: should begin with v=DMARCv1, seen v=%q", v)
		}

		version, err := strconv.ParseUint(v[5:], 10, 32)
		if err != nil {
			return fmt.Errorf("not a valid DMARC record: bad version number: %w", err)
		}
		t.Version = uint(version)
	} else {
		return fmt.Errorf("not a valid DMARC record: version not found")
	}

	if p, ok := fields["p"]; ok {
		t.Request = p
	}
	if sp, ok := fields["sp"]; ok {
		t.SRequest = sp
	}
	if rua, ok := fields["rua"]; ok {
		t.AURI = strings.Split(rua, ",")
	}
	if ruf, ok := fields["ruf"]; ok {
		t.FURI = strings.Split(ruf, ",")
	}
	if adkim, ok := fields["adkim"]; ok && adkim == "s" {
		t.ADKIM = true
	}
	if aspf, ok := fields["aspf"]; ok && aspf == "s" {
		t.ASPF = true
	}
	if ri, ok := fields["ri"]; ok {
		v, err := strconv.ParseUint(ri, 10, 32)
		if err != nil {
			return fmt.Errorf("not a valid DMARC record: bad interval value (ri): %w", err)
		}

		t.AInterval = common.Duration(v)
	} else {
		t.AInterval = 86400
	}
	if fo, ok := fields["fo"]; ok {
		t.FailureOptions = strings.Split(fo, ":")
	}
	if rf, ok := fields["rf"]; ok {
		t.RegisteredFormats = strings.Split(rf, ":")
	}
	if pct, ok := fields["pct"]; ok {
		v, err := strconv.ParseUint(pct, 10, 8)
		if err != nil {
			return fmt.Errorf("not a valid DMARC record: bad percent value (prc): %w", err)
		}

		t.Percent = uint8(v)
	} else {
		t.Percent = 100
	}

	return nil
}

func (t *DMARCFields) String() string {
	fields := []string{
		fmt.Sprintf("v=DMARC%d", t.Version),
	}

	if t.Request != "" {
		fields = append(fields, fmt.Sprintf("p=%s", t.Request))
	}
	if t.SRequest != "" {
		fields = append(fields, fmt.Sprintf("sp=%s", t.SRequest))
	}
	if len(t.AURI) > 0 {
		fields = append(fields, fmt.Sprintf("rua=%s", strings.Join(t.AURI, ",")))
	}
	if len(t.FURI) > 0 {
		fields = append(fields, fmt.Sprintf("ruf=%s", strings.Join(t.FURI, ",")))
	}
	if t.ADKIM {
		fields = append(fields, "adkim=s")
	} else {
		fields = append(fields, "adkim=r")
	}
	if t.ASPF {
		fields = append(fields, "aspf=s")
	} else {
		fields = append(fields, "aspf=r")
	}
	if t.AInterval != 86400 && t.AInterval != 0 {
		fields = append(fields, fmt.Sprintf("ri=%d", t.AInterval))
	}
	if len(t.FailureOptions) > 0 {
		fields = append(fields, fmt.Sprintf("fo=%s", strings.Join(t.FailureOptions, ":")))
	}
	if len(t.RegisteredFormats) > 0 {
		fields = append(fields, fmt.Sprintf("rf=%s", strings.Join(t.RegisteredFormats, ":")))
	}
	if t.Percent != 100 {
		fields = append(fields, fmt.Sprintf("pct=%d", t.Percent))
	}

	return strings.Join(fields, ";")
}

func dmarc_analyze(a *svc.Analyzer) (err error) {
	for _, record := range a.SearchRR(svc.AnalyzerRecordFilter{Type: dns.TypeTXT, Prefix: "_dmarc"}) {
		domain := strings.TrimPrefix(record.Header().Name, "_dmarc.")

		err = a.UseRR(record, domain, &DMARC{
			Record: helpers.RRRelativeSubdomain(record, a.GetOrigin(), domain).(*happydns.TXT),
		})
		if err != nil {
			return
		}
	}

	return
}

func init() {
	svc.RegisterService(
		func() happydns.ServiceBody {
			return &DMARC{}
		},
		dmarc_analyze,
		happydns.ServiceInfos{
			Name:        "DMARC",
			Description: "Domain-based Message Authentication, Reporting and Conformance.",
			Categories: []string{
				"email",
			},
			RecordTypes: []uint16{
				dns.TypeTXT,
			},
			Restrictions: happydns.ServiceRestrictions{
				NearAlone: true,
				NeedTypes: []uint16{
					dns.TypeTXT,
				},
			},
		},
		1,
	)
}
