package vulnscan

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

const defaultOSVAPI = "https://api.osv.dev/v1"

// Client queries the OSV (Open Source Vulnerabilities) API.
// https://osv.dev/docs/
type Client struct {
	baseURL string
	http    *http.Client
}

// NewClient creates a client that queries the public OSV API.
func NewClient() *Client {
	return &Client{
		baseURL: defaultOSVAPI,
		http: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// QueryRequest is sent to POST /v1/query.
type QueryRequest struct {
	Package  PackageID `json:"package"`
	Version  string    `json:"version"`
}

// PackageID identifies a package in a specific ecosystem.
type PackageID struct {
	PURL string `json:"purl,omitempty"`
	Name string `json:"name,omitempty"`
	Ecosystem string `json:"ecosystem,omitempty"`
}

// QueryResponse is the response from POST /v1/query.
type QueryResponse struct {
	Vulns []OSVVuln `json:"vulns"`
}

// OSVVuln is a vulnerability returned by the OSV API.
type OSVVuln struct {
	ID         string   `json:"id"`
	Summary    string   `json:"summary"`
	Details    string   `json:"details"`
	Aliases    []string `json:"aliases"`
	Fixed      string   `json:"fixed,omitempty"`
	Severity   []Severity `json:"severity,omitempty"`
	Affected   []Affected `json:"affected,omitempty"`
	Published  string   `json:"published,omitempty"`
	Modified   string   `json:"modified,omitempty"`
}

// Severity holds a CVSS score from the OSV response.
type Severity struct {
	Type  string `json:"type"`
	Score string `json:"score"`
}

// Affected describes a package version range.
type Affected struct {
	Package  PackageID     `json:"package"`
	Ranges   []AffectedRange `json:"ranges"`
	Versions []string      `json:"versions"`
}

type AffectedRange struct {
	Type   string             `json:"type"`
	Events []RangeEvent       `json:"events"`
}

type RangeEvent struct {
	Introduced string `json:"introduced"`
	Fixed      string `json:"fixed"`
	Limit      string `json:"limit"`
}

// QueryByPURL queries OSV for vulnerabilities affecting a given PURL + version.
func (c *Client) QueryByPURL(purl, version string) ([]OSVVuln, error) {
	body := QueryRequest{
		Package: PackageID{PURL: purl},
		Version: version,
	}
	return c.doQuery(body)
}

// QueryByEcosystem queries OSV for vulnerabilities affecting a package in a
// specific ecosystem (e.g. "npm", "Go", "PyPI", "cargo", "Maven", "RubyGems").
func (c *Client) QueryByEcosystem(ecosystem, name, version string) ([]OSVVuln, error) {
	body := QueryRequest{
		Package: PackageID{
			Name:      name,
			Ecosystem: ecosystem,
		},
		Version: version,
	}
	return c.doQuery(body)
}

func (c *Client) doQuery(body interface{}) ([]OSVVuln, error) {
	payload, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("vulnscan: marshal body: %w", err)
	}

	req, err := http.NewRequest(http.MethodPost, c.baseURL+"/query", bytes.NewReader(payload))
	if err != nil {
		return nil, fmt.Errorf("vulnscan: create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, fmt.Errorf("vulnscan: query: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("vulnscan: read response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("vulnscan: OSV returned %d: %s", resp.StatusCode, string(respBody))
	}

	var qr QueryResponse
	if err := json.Unmarshal(respBody, &qr); err != nil {
		return nil, fmt.Errorf("vulnscan: parse response: %w", err)
	}
	return qr.Vulns, nil
}
