package gemini

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

// https://ai.google.dev/api/generate-content#request-body
type Request struct {
	Contents          []Content         `json:"contents"`
	Tools             []Tool            `json:"tools,omitempty"`
	SystemInstruction *Content          `json:"systemInstruction,omitempty"`
	GenerationConfig  *GenerationConfig `json:"generationConfig,omitempty"`
	CachedContent     string            `json:"cachedContent,omitempty"` // format: "cachedContents/{name}"
	// ToolConfig has been left out because it does not appear to be useful.
}

// https://ai.google.dev/api/generate-content#response-body
type Response struct {
	Candidates []Candidate `json:"candidates"`
	headers    http.Header // captured HTTP response headers
}

// Header returns the HTTP response headers.
func (r *Response) Header() http.Header {
	return r.headers
}

type Candidate struct {
	Content Content `json:"content"`
}

type Content struct {
	Parts []Part `json:"parts"`
	Role  string `json:"role,omitempty"`
}

// Part is a part of the content.
// This is a union data structure, only one-of the fields can be set.
type Part struct {
	Text                string               `json:"text,omitempty"`
	FunctionCall        *FunctionCall        `json:"functionCall,omitempty"`
	FunctionResponse    *FunctionResponse    `json:"functionResponse,omitempty"`
	ExecutableCode      *ExecutableCode      `json:"executableCode,omitempty"`
	CodeExecutionResult *CodeExecutionResult `json:"codeExecutionResult,omitempty"`
	// TODO inlineData
	// TODO fileData
}

type FunctionCall struct {
	Name string         `json:"name"`
	Args map[string]any `json:"args"`
}

type FunctionResponse struct {
	Name     string         `json:"name"`
	Response map[string]any `json:"response"`
}

type ExecutableCode struct {
	Language Language `json:"language"`
	Code     string   `json:"code"`
}

type Language int

const (
	LanguageUnspecified Language = 0
	LanguagePython      Language = 1 // python >= 3.10 with numpy and simpy
)

type CodeExecutionResult struct {
	Outcome Outcome `json:"outcome"`
	Output  string  `json:"output"`
}

type Outcome int

const (
	OutcomeUnspecified      Outcome = 0
	OutcomeOK               Outcome = 1
	OutcomeFailed           Outcome = 2
	OutcomeDeadlineExceeded Outcome = 3
)

// https://ai.google.dev/api/generate-content#v1beta.GenerationConfig
type GenerationConfig struct {
	ResponseMimeType string  `json:"responseMimeType,omitempty"` // text/plain, application/json, or text/x.enum
	ResponseSchema   *Schema `json:"responseSchema,omitempty"`   // for JSON
}

// https://ai.google.dev/api/caching#Tool
type Tool struct {
	FunctionDeclarations []FunctionDeclaration `json:"functionDeclarations"`
	CodeExecution        *struct{}             `json:"codeExecution,omitempty"` // if present, enables the model to execute code
	// TODO googleSearchRetrieval https://ai.google.dev/api/caching#GoogleSearchRetrieval
}

// https://ai.google.dev/api/caching#FunctionDeclaration
type FunctionDeclaration struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Parameters  Schema `json:"parameters"`
}

// https://ai.google.dev/api/caching#Schema
type Schema struct {
	Type        DataType          `json:"type"`
	Format      string            `json:"string,omitempty"` // for NUMBER type: float, double for INTEGER type: int32, int64 for STRING type: enum
	Description string            `json:"description,omitempty"`
	Nullable    *bool             `json:"nullable,omitempty"`
	Enum        []string          `json:"enum,omitempty"`
	MaxItems    string            `json:"maxItems,omitempty"`   // for ARRAY
	MinItems    string            `json:"minItems,omitempty"`   // for ARRAY
	Properties  map[string]Schema `json:"properties,omitempty"` // for OBJECT
	Required    []string          `json:"required,omitempty"`   // for OBJECT
	Items       *Schema           `json:"items,omitempty"`      // for ARRAY
}

type DataType int

const (
	DataTypeUNSPECIFIED = DataType(0) // Not specified, should not be used.
	DataTypeSTRING      = DataType(1)
	DataTypeNUMBER      = DataType(2)
	DataTypeINTEGER     = DataType(3)
	DataTypeBOOLEAN     = DataType(4)
	DataTypeARRAY       = DataType(5)
	DataTypeOBJECT      = DataType(6)
)

const defaultEndpoint = "https://generativelanguage.googleapis.com/v1beta"

type Model struct {
	Model    string // e.g. "models/gemini-1.5-flash"
	APIKey   string
	HTTPC    *http.Client // if nil, http.DefaultClient is used
	Endpoint string       // if empty, DefaultEndpoint is used
}

func (m Model) GenerateContent(ctx context.Context, req *Request) (*Response, error) {
	reqBytes, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshaling request: %w", err)
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/%s:generateContent?key=%s", m.endpoint(), m.Model, m.APIKey), bytes.NewReader(reqBytes))
	if err != nil {
		return nil, fmt.Errorf("creating HTTP request: %w", err)
	}
	httpReq.Header.Add("Content-Type", "application/json")
	httpResp, err := m.httpc().Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("GenerateContent: do: %w", err)
	}
	defer httpResp.Body.Close()
	body, err := io.ReadAll(httpResp.Body)
	if err != nil {
		return nil, fmt.Errorf("GenerateContent: reading response body: %w", err)
	}
	if httpResp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("GenerateContent: HTTP status: %d, %s", httpResp.StatusCode, string(body))
	}
	var res Response
	if err := json.Unmarshal(body, &res); err != nil {
		return nil, fmt.Errorf("GenerateContent: unmarshaling response: %w, %s", err, string(body))
	}
	res.headers = httpResp.Header
	return &res, nil
}

func (m Model) endpoint() string {
	if m.Endpoint != "" {
		return m.Endpoint
	}
	return defaultEndpoint
}

func (m Model) httpc() *http.Client {
	if m.HTTPC != nil {
		return m.HTTPC
	}
	return http.DefaultClient
}
