// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package file

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/rs/zerolog/log"

	"go.woodpecker-ci.org/woodpecker/v3/pipeline"
	"go.woodpecker-ci.org/woodpecker/v3/server/model"
	service_log "go.woodpecker-ci.org/woodpecker/v3/server/services/log"
)

const (
	// Add base64 overhead and space for other JSON fields (just to be safe).
	maxLineLength int = (pipeline.MaxLogLineLength/3)*4 + (64 * 1024) //nolint:mnd
)

type logStore struct {
	base string
}

func NewLogStore(base string) (service_log.Service, error) {
	if base == "" {
		return nil, fmt.Errorf("file storage base path is required")
	}
	if _, err := os.Stat(base); err != nil && os.IsNotExist(err) {
		err = os.MkdirAll(base, 0o700)
		if err != nil {
			return nil, err
		}
	}
	return logStore{base: base}, nil
}

func (l logStore) filePath(id int64) string {
	return filepath.Join(l.base, fmt.Sprintf("%d.json", id))
}

func (l logStore) LogFind(step *model.Step) ([]*model.LogEntry, error) {
	filename := l.filePath(step.ID)
	file, err := os.Open(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	defer file.Close()

	buf := make([]byte, 0, bufio.MaxScanTokenSize)
	s := bufio.NewScanner(file)
	s.Buffer(buf, maxLineLength)

	var entries []*model.LogEntry
	for s.Scan() {
		j := s.Text()
		if len(strings.TrimSpace(j)) == 0 {
			continue
		}
		entry := &model.LogEntry{}
		err = json.Unmarshal([]byte(j), entry)
		if err != nil {
			return nil, err
		}
		entries = append(entries, entry)
	}

	return entries, nil
}

func (l logStore) LogAppend(step *model.Step, logEntries []*model.LogEntry) error {
	path := l.filePath(step.ID)

	file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
	if err != nil {
		log.Error().Err(err).Msgf("could not open log file %s", path)
		return err
	}

	var bytes []byte

	for _, logEntry := range logEntries {
		if jsonLine, err := json.Marshal(logEntry); err == nil {
			bytes = append(bytes, jsonLine...)
			bytes = append(bytes, byte('\n'))
		} else {
			log.Error().Err(err).Msg("could not convert log entry to JSON")
		}
	}

	if _, err = file.Write(bytes); err != nil {
		log.Error().Err(err).Msg("could not write out log entries")
	}

	return file.Close()
}

func (l logStore) LogDelete(step *model.Step) error {
	return os.Remove(l.filePath(step.ID))
}

func (l logStore) StepFinished(_ *model.Step) {}
