// SPDX-FileCopyrightText: 2024 Nicolas Peugnet <nicolas@club1.fr>
// SPDX-License-Identifier: GPL-3.0-or-later

package lintian

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"os"
	"os/exec"
	"path"
	"strings"
	"time"

	"salsa.debian.org/lintian/lintian-ssg/markdown"
)

const sourceURLFmt = "https://salsa.debian.org/lintian/lintian/-/blob/%s/tags/%s.tag"

type Screen struct {
	Advocates []string `json:"advocates"`
	Name      string   `json:"name"`
	Reason    string   `json:"reason"`
	SeeAlso   []string `json:"see_also"`
}

func (s *Screen) AdvocatesHTML() template.HTML {
	return markdown.ToHTML(strings.Join(s.Advocates, ", "), markdown.StyleInline)
}

func (s *Screen) ReasonHTML() template.HTML {
	return markdown.ToHTML(s.Reason, markdown.StyleFull)
}

func (s *Screen) SeeAlsoHTML() template.HTML {
	return markdown.ToHTML("See also: "+strings.Join(s.SeeAlso, ", "), markdown.StyleInline)
}

type Level string

const (
	LevelError          Level = "error"
	LevelWarning        Level = "warning"
	LevelInfo           Level = "info"
	LevelPedantic       Level = "pedantic"
	LevelClassification Level = "classification"
)

type Tag struct {
	Name           string   `json:"name"`
	NameSpaced     bool     `json:"name_spaced"`
	Visibility     Level    `json:"visibility"`
	Explanation    string   `json:"explanation"`
	SeeAlso        []string `json:"see_also"`
	RenamedFrom    []string `json:"renamed_from"`
	Experimental   bool     `json:"experimental"`
	LintianVersion string   `json:"lintian_version"`
	Screens        []Screen `json:"screens"`
}

func (t *Tag) ExplanationHTML() template.HTML {
	return markdown.ToHTML(t.Explanation, markdown.StyleFull)
}

func (t *Tag) SeeAlsoHTML() []template.HTML {
	seeAlsoHTML := make([]template.HTML, len(t.SeeAlso))
	for i, str := range t.SeeAlso {
		seeAlsoHTML[i] = markdown.ToHTML(str, markdown.StyleInline)
	}
	return seeAlsoHTML
}

func (t *Tag) RenamedFromStr() string {
	return strings.Join(t.RenamedFrom, ", ")
}

func (t *Tag) Source() string {
	name := t.Name
	if !t.NameSpaced {
		name = path.Join(string(name[0]), name)
	}
	return fmt.Sprintf(sourceURLFmt, t.LintianVersion, name)
}

func decodeTags(r io.Reader, fn func(*Tag)) error {
	jsonTagsDecoder := json.NewDecoder(r)

	// discard open bracket
	_, err := jsonTagsDecoder.Token()
	if err != nil {
		return fmt.Errorf("json: %w", err)
	}

	// while the array contains values
	for jsonTagsDecoder.More() {
		var tag Tag
		if err := jsonTagsDecoder.Decode(&tag); err != nil {
			return fmt.Errorf("json: %w", err)
		}
		fn(&tag)
	}

	// discard closing bracket
	_, err = jsonTagsDecoder.Token()
	if err != nil {
		return fmt.Errorf("json: %w", err)
	}
	return nil
}

type Stats struct {
	UserTime   time.Duration
	SystemTime time.Duration
}

// systemTagsProvider is a tags provider that calls json-explain-tags.
type systemTagsProvider struct {
	cmd    *exec.Cmd
	reader io.Reader
}

func (p *systemTagsProvider) ForEach(callback func(tag *Tag)) error {
	return decodeTags(p.reader, callback)
}

func (p *systemTagsProvider) Wait() (*Stats, error) {
	if err := p.cmd.Wait(); err != nil {
		return nil, err
	}
	return &Stats{
		p.cmd.ProcessState.UserTime(),
		p.cmd.ProcessState.SystemTime(),
	}, nil
}

// fileTagsProvider is a tags provider that simply reads a file.
type fileTagsProvider struct {
	reader io.Reader
}

func (p *fileTagsProvider) ForEach(callback func(tag *Tag)) error {
	return decodeTags(p.reader, callback)
}

func (p *fileTagsProvider) Wait() (*Stats, error) {
	return &Stats{}, nil
}

// TagsProvider is an interface that describes a provider of Lintian Tags.
type TagsProvider interface {
	// ForEach iterates over the tags and calls the callback function
	// with each of them. It can only be called one time by provider.
	ForEach(callback func(tag *Tag)) error
	// Wait must be called after ForEach finishes.
	Wait() (*Stats, error)
}

// NewSystemTagsProvider returns a tags provider that calls json-explain-tags.
func NewSystemTagsProvider() (TagsProvider, error) {
	cmd := exec.Command("lintian-explain-tags", "--format=json")
	cmd.Stderr = os.Stderr
	reader, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	return &systemTagsProvider{cmd, reader}, cmd.Start()
}

// NewFileTagsProvider returns a tags provider that simply reads a file.
func NewFileTagsProvider(path string) (TagsProvider, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	return &fileTagsProvider{file}, nil
}
