// Copyright 2023 Google LLC

// 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

//     https://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 sshca is the actual writing end of the sshtrustedca pipeline.
package sshca

import (
	"context"
	"encoding/json"
	"strings"

	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/events"
	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/events/sshtrustedca"
	"github.com/GoogleCloudPlatform/guest-agent/metadata"
	"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)

// Certificates wrapps a list of certificate authorities.
type Certificates struct {
	Certs []TrustedCert `json:"trustedCertificateAuthorities"`
}

// TrustedCert defines the object containing a public key.
type TrustedCert struct {
	PublicKey string `json:"publicKey"`
}

var (
	// mdsClient is the metadata's client, used to query oslogin certificates.
	mdsClient *metadata.Client
)

// Init initializes the sshca's event handler callback.
func Init() {
	mdsClient = metadata.New()
	events.Get().Subscribe(sshtrustedca.ReadEvent, nil, writeFile)
}

// Close finishes the sshca module, deallocating everything allocated with Init().
func Close() {
	events.Get().Unsubscribe(sshtrustedca.ReadEvent, writeFile)
	mdsClient = nil
}

// writeFile is an event handler callback and writes the actual sshca content to the pipe
// used by openssh to grant access based on ssh ca.
func writeFile(ctx context.Context, evType string, data interface{}, evData *events.EventData) bool {
	// There was some error on the pipe watcher, just ignore it.
	if evData.Error != nil {
		logger.Debugf("Not handling ssh trusted ca cert event, we got an error: %+v", evData.Error)
		return true
	}

	// Make sure we close the pipe after we've done writing to it.
	pipeData, ok := evData.Data.(*sshtrustedca.PipeData)
	if !ok {
		logger.Errorf("Received invalid event data (%+v), ignoring this event and un-subscribing %s", evData.Data, evType)
		return false
	}

	defer func() {
		if err := pipeData.File.Close(); err != nil {
			logger.Errorf("Failed to close pipe: %+v", err)
		}
		pipeData.Finished()
	}()

	certificate, err := mdsClient.GetKey(ctx, "oslogin/certificates", nil)
	if err != nil {
		logger.Errorf("Failed to get certificate from metadata server: %+v", err)
		return true
	}

	// Keep a copy of the returned certificate for error fallback caching.
	var certs Certificates
	var outData []string

	if err := json.Unmarshal([]byte(certificate), &certs); err != nil {
		logger.Errorf("Failed to unmarshal certificate json: %+v", err)
		return true
	}

	for _, curr := range certs.Certs {
		outData = append(outData, curr.PublicKey)
	}

	outStr := strings.Join(outData, "\n")
	n, err := pipeData.File.WriteString(outStr)
	if err != nil {
		logger.Errorf("Failed to write certificate to the write end of the pipe: %+v", err)
		return true
	}

	if n != len(outStr) {
		logger.Errorf("Wrote the wrong ammout of data, wrote %d bytes instead of %d bytes", n, len(certificate))
	}

	return true
}
