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

//go:generate ../devtools/generate_discovery_client.sh

import (
	"context"
	"io"
	"os"
	"strings"

	gl "cloud.google.com/go/ai/generativelanguage/apiv1beta"
	pb "cloud.google.com/go/ai/generativelanguage/apiv1beta/generativelanguagepb"
	gld "github.com/google/generative-ai-go/genai/internal/generativelanguage/v1beta" // discovery client
	"google.golang.org/api/googleapi"
	"google.golang.org/api/iterator"
)

// UploadFileOptions are options for [Client.UploadFile].
type UploadFileOptions struct {
	// A more readable name for the file.
	DisplayName string

	// The IANA standard MIME type of the file. It will be stored with the file as metadata.
	// If omitted, the service will try to infer it. You may instead wish to use
	// [http.DetectContentType].
	// The supported MIME types are documented on [this page].
	//
	// [this page]: https://ai.google.dev/gemini-api/docs/prompting_with_media?lang=go#supported_file_formats
	MIMEType string
}

// UploadFile copies the contents of the given io.Reader to file storage associated
// with the service, and returns information about the resulting file.
//
// The name is a relatively short, unique identifier for the file (rather than a typical
// filename).
// Typically it should be left empty, in which case a unique name will be generated.
// Otherwise, it can contain up to 40 characters that are lowercase
// alphanumeric or dashes (-), not starting or ending with a dash.
// To generate your own unique names, consider a cryptographic hash algorithm like SHA-1.
// The string "files/" is prepended to the name if it does not contain a '/'.
//
// Use the returned file's URI field with a [FileData] Part to use it for generation.
//
// It is an error to upload a file that already exists.
func (c *Client) UploadFile(ctx context.Context, name string, r io.Reader, opts *UploadFileOptions) (*File, error) {
	if name != "" {
		name = userNameToServiceName(name)
	}
	req := &gld.CreateFileRequest{
		File: &gld.File{Name: name},
	}
	if opts != nil && opts.DisplayName != "" {
		req.File.DisplayName = opts.DisplayName
	}
	call := c.ds.Media.Upload(req)
	var mopts []googleapi.MediaOption
	if opts != nil && opts.MIMEType != "" {
		mopts = append(mopts, googleapi.ContentType(opts.MIMEType))
	}
	call.Media(r, mopts...)
	res, err := call.Do()
	if err != nil {
		return nil, err
	}
	// Don't return the result, because it contains a file as represented by the
	// discovery client and we'd have to write code to convert it to this package's
	// File type.
	// Instead, make a GetFile call to get the proto file, which our generated code can convert.
	return c.GetFile(ctx, res.File.Name)
}

// UploadFileFromPath is a convenience method wrapping [UploadFile]. It takes
// a path to read the file from, and uses a default auto-generated ID for the
// uploaded file.
func (c *Client) UploadFileFromPath(ctx context.Context, path string, opts *UploadFileOptions) (*File, error) {
	osf, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer osf.Close()

	return c.UploadFile(ctx, "", osf, opts)
}

// GetFile returns the named file.
func (c *Client) GetFile(ctx context.Context, name string) (*File, error) {
	req := &pb.GetFileRequest{Name: userNameToServiceName(name)}
	debugPrint(req)
	pf, err := c.fc.GetFile(ctx, req)
	if err != nil {
		return nil, err
	}
	return (File{}).fromProto(pf), nil
}

// DeleteFile deletes the file with the given name.
// It is an error to delete a file that does not exist.
func (c *Client) DeleteFile(ctx context.Context, name string) error {
	req := &pb.DeleteFileRequest{Name: userNameToServiceName(name)}
	debugPrint(req)
	return c.fc.DeleteFile(ctx, req)
}

// userNameToServiceName converts a name supplied by the user to a name required by the service.
func userNameToServiceName(name string) string {
	if strings.ContainsRune(name, '/') {
		return name
	}
	return "files/" + name
}

// ListFiles returns an iterator over the uploaded files.
func (c *Client) ListFiles(ctx context.Context) *FileIterator {
	return &FileIterator{
		it: c.fc.ListFiles(ctx, &pb.ListFilesRequest{}),
	}
}

// A FileIterator iterates over Files.
type FileIterator struct {
	it *gl.FileIterator
}

// Next returns the next result. Its second return value is iterator.Done if there are no more
// results. Once Next returns Done, all subsequent calls will return Done.
func (it *FileIterator) Next() (*File, error) {
	m, err := it.it.Next()
	if err != nil {
		return nil, err
	}
	return (File{}).fromProto(m), nil
}

// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
func (it *FileIterator) PageInfo() *iterator.PageInfo {
	return it.it.PageInfo()
}

// FileMetadata holds metadata about a file.
type FileMetadata struct {
	// Set if the file contains video.
	Video *VideoMetadata
}

func populateFileTo(p *pb.File, f *File) {
	p.Metadata = nil
	if f == nil || f.Metadata == nil {
		return
	}
	if f.Metadata.Video != nil {
		p.Metadata = &pb.File_VideoMetadata{
			VideoMetadata: f.Metadata.Video.toProto(),
		}
	}
}

func populateFileFrom(f *File, p *pb.File) {
	f.Metadata = nil
	if p == nil || p.Metadata == nil {
		return
	}

	if p.Metadata != nil {
		switch m := p.Metadata.(type) {
		case *pb.File_VideoMetadata:
			f.Metadata = &FileMetadata{
				Video: (VideoMetadata{}).fromProto(m.VideoMetadata),
			}
		default:
			// ignore other types
			// TODO: signal a problem
		}
	}
}
