/*
concourse-debian-dsa a Concourse CI resource type to get Debian security update information

Copyright Jan Dittberner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package dsa

import (
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"sort"
	"strings"
	"time"
)

const rdfURL = "https://www.debian.org/security/dsa"
const rdfLongURL = "https://www.debian.org/security/dsa-long"
const isoDateFormat = "2006-01-02"

var ErrNotSupported = errors.New("unsupported operation")

func parseISODate(dateStr string) (time.Time, error) {
	date, err := time.Parse(isoDateFormat, dateStr)
	if err != nil {
		return time.Time{}, fmt.Errorf("could not parse date: %w", err)
	}

	return date, nil
}

func getRdfData(full bool) (*rdfData, error) {
	client := &http.Client{}

	var url string

	if full {
		url = rdfLongURL
	} else {
		url = rdfURL
	}

	resp, err := client.Get(url)
	if err != nil {
		return nil, fmt.Errorf("could not fetch Debian Security Announcement RDF: %w", err)
	}

	defer func() { _ = resp.Body.Close() }()

	rdfBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("could not read RDF data from response: %w", err)
	}

	data := &rdfData{}

	err = xml.Unmarshal(rdfBytes, data)
	if err != nil {
		return nil, fmt.Errorf("could not parse RDF data: %w", err)
	}

	return data, nil
}

type Version struct {
	Date      string `json:"date"`
	DSANumber string `json:"dsa"`
}

func (v Version) String() string {
	data, _ := json.Marshal(v)

	return string(data)
}

type checkInput struct {
	Source  any     `json:"source"`
	Version Version `json:"version"`
}

type getInput struct {
	Source  any     `json:"source"`
	Version Version `json:"version"`
	Params  any     `json:"params"`
}

type MetaData struct {
	Link        string `json:"link"`
	Package     string `json:"package"`
	Description string `json:"description"`
}

type getOutput struct {
	Version  Version  `json:"version"`
	Metadata MetaData `json:"metadata"`
}

type Resource struct {
	in  io.Reader
	out io.Writer
}

type VersionRange struct {
	versions []Version
}

func (r *VersionRange) Len() int {
	return len(r.versions)
}

func (r *VersionRange) Less(i, j int) bool {
	return r.versions[i].Date <= r.versions[j].Date && r.versions[i].DSANumber < r.versions[j].DSANumber
}

func (r *VersionRange) Swap(i, j int) {
	tmpVer := r.versions[i]

	r.versions[i] = r.versions[j]
	r.versions[j] = tmpVer
}

func (r *VersionRange) addVersion(version Version) {
	r.versions = append(r.versions, version)
}

func (r *Resource) Check() error {
	input, err := r.readCheckInput()
	if err != nil {
		return fmt.Errorf("could not parse input from Concourse: %w", err)
	}

	if len(input.Version.Date) > 0 {
		_, err = parseISODate(input.Version.Date)
		if err != nil {
			return fmt.Errorf("could not interpret version from Concourse as date: %w", err)
		}
	}

	dsaData, err := getRdfData(false)
	if err != nil {
		log.Fatalf("could not get DSA RDF data: %v", err)
	}

	dates := &VersionRange{}

	for _, item := range dsaData.Items {
		parts := strings.SplitN(item.Title, " ", 2)

		dates.addVersion(Version{Date: item.Date, DSANumber: parts[0]})
	}

	sort.Sort(dates)

	err = r.writeCheckOutput(dates)
	if err != nil {
		return fmt.Errorf("could not write output for Concourse: %w", err)
	}

	return nil
}

func (r *Resource) Get(_ string) error {
	input, err := r.readGetInput()
	if err != nil {
		return fmt.Errorf("could not parse input from Concourse: %w", err)
	}

	_, err = parseISODate(input.Version.Date)
	if err != nil {
		return fmt.Errorf("could not interpret version from Concourse as date: %w", err)
	}

	dsaData, err := getRdfData(true)
	if err != nil {
		log.Fatalf("could not get DSA RDF data: %v", err)
	}

	for _, item := range dsaData.Items {
		if item.Date == input.Version.Date && strings.HasPrefix(item.Title, input.Version.DSANumber) {
			parts := strings.SplitN(item.Title, " ", 3)

			err = r.writeGetOutput(Version{
				Date:      item.Date,
				DSANumber: parts[0],
			}, MetaData{
				Link:    item.Link,
				Package: parts[1],
				Description: strings.TrimSpace(
					strings.ReplaceAll(strings.ReplaceAll(
						item.Description, "<p>", ""), "</p>", "\n",
					),
				),
			})

			if err != nil {
				return fmt.Errorf("could not write output for Concourse: %w", err)
			}

			return nil
		}
	}

	return fmt.Errorf("requested version %s not found", input.Version)
}

func (r *Resource) readCheckInput() (*checkInput, error) {
	dec := json.NewDecoder(r.in)

	inp := &checkInput{}

	err := dec.Decode(inp)
	if err != nil {
		return nil, fmt.Errorf("could not decode Concourse input: %w", err)
	}

	return inp, nil
}

func (r *Resource) writeCheckOutput(versions *VersionRange) error {
	enc := json.NewEncoder(r.out)

	err := enc.Encode(versions.versions)
	if err != nil {
		return fmt.Errorf("could not marshal JSON: %w", err)
	}

	return nil
}

func (r *Resource) readGetInput() (*getInput, error) {
	dec := json.NewDecoder(r.in)

	inp := &getInput{}

	err := dec.Decode(inp)
	if err != nil {
		return nil, fmt.Errorf("could not decode Concourse input: %w", err)
	}

	return inp, nil
}

func (r *Resource) writeGetOutput(version Version, metaData MetaData) error {
	enc := json.NewEncoder(r.out)

	output := &getOutput{
		Version:  version,
		Metadata: metaData,
	}

	err := enc.Encode(&output)
	if err != nil {
		return fmt.Errorf("could not marshal JSON: %w", err)
	}

	return nil
}

func NewResource(in io.Reader, out io.Writer) *Resource {
	return &Resource{in: in, out: out}
}
