package stub

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

	"github.com/tidbcloud/controlplane9/internal/host"
)

const (
	EnvRecordFile      = "CP9_RECORD_FILE"
	EnvBillingEndpoint = "CP9_BILLING_ENDPOINT"

	RecordingMountPath    = "/cp9/stub-state"
	RecordingNodeStateDir = host.RemoteNodeStateDir

	MeteringRecordRoute = "/recording/payloads"
	BillingRecordRoute  = "/recording/metering"
)

const recordingFilePerm = 0o644

// MeteringPayload is the JSONL shape written by the metering recorder stub.
type MeteringPayload struct {
	ClusterID            string         `json:"cluster_id"`
	Status               string         `json:"status"`
	WindowStart          time.Time      `json:"window_start"`
	WindowEnd            time.Time      `json:"window_end"`
	VCPUByComponent      map[string]int `json:"vcpu_by_component"`
	TotalPurchasedVCPU   int            `json:"total_purchased_vcpu"`
	UnitPricePerVCPUHour float64        `json:"unit_price_per_vcpu_hour,omitempty"`
}

// BillingInvoice is the JSONL shape written by the billing recorder stub.
type BillingInvoice struct {
	ClusterID            string    `json:"cluster_id"`
	Status               string    `json:"status"`
	WindowStart          time.Time `json:"window_start"`
	WindowEnd            time.Time `json:"window_end"`
	Billable             bool      `json:"billable"`
	BillableDurationSecs int64     `json:"billable_duration_secs"`
	TotalPurchasedVCPU   int       `json:"total_purchased_vcpu"`
	UnitPricePerVCPUHour float64   `json:"unit_price_per_vcpu_hour"`
	InvoiceTotal         float64   `json:"invoice_total"`
}

func MeteringPayloadsPath(stateDir string) string {
	return filepath.Join(stateDir, "stubs", "metering", "payloads.jsonl")
}

func BillingInvoicesPath(stateDir string) string {
	return filepath.Join(stateDir, "stubs", "billing-svc", "invoices.jsonl")
}

func LoadMeteringPayloads(stateDir string) ([]MeteringPayload, error) {
	return decodeJSONL[MeteringPayload](MeteringPayloadsPath(stateDir))
}

func LoadBillingInvoices(stateDir string) ([]BillingInvoice, error) {
	return decodeJSONL[BillingInvoice](BillingInvoicesPath(stateDir))
}

func FilterMeteringPayloads(payloads []MeteringPayload, clusterID string) []MeteringPayload {
	var filtered []MeteringPayload
	for _, payload := range payloads {
		if payload.ClusterID == clusterID {
			filtered = append(filtered, payload)
		}
	}
	return filtered
}

func FilterBillingInvoices(invoices []BillingInvoice, clusterID string) []BillingInvoice {
	var filtered []BillingInvoice
	for _, invoice := range invoices {
		if invoice.ClusterID == clusterID {
			filtered = append(filtered, invoice)
		}
	}
	return filtered
}

func RecordingKindForService(service string) Kind {
	switch service {
	case "metering":
		return KindMeteringRecorder
	case "billing-svc":
		return KindBillingRecorder
	case "dataplane-manager":
		return KindDataplaneManager
	default:
		return KindPermissive
	}
}

func IsRecordingKind(kind Kind) bool {
	return kind == KindMeteringRecorder || kind == KindBillingRecorder || kind == KindDataplaneManager
}

func RecordingFileName(kind Kind) string {
	switch kind {
	case KindMeteringRecorder:
		return "payloads.jsonl"
	case KindBillingRecorder:
		return "invoices.jsonl"
	case KindDataplaneManager:
		return "boundary.jsonl"
	default:
		return ""
	}
}

func RecordingNodeServiceDir(service string) string {
	return path.Join(RecordingNodeStateDir, "stubs", service)
}

func RecordingContainerFile(kind Kind) string {
	fileName := RecordingFileName(kind)
	if fileName == "" {
		return ""
	}
	return path.Join(RecordingMountPath, fileName)
}

func AppendJSONL(filePath string, value any) error {
	if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
		return fmt.Errorf("mkdir %s: %w", filepath.Dir(filePath), err)
	}
	f, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, recordingFilePerm)
	if err != nil {
		return fmt.Errorf("open %s: %w", filePath, err)
	}
	defer f.Close()

	data, err := json.Marshal(value)
	if err != nil {
		return fmt.Errorf("marshal %s: %w", filePath, err)
	}
	if _, err := f.Write(append(data, '\n')); err != nil {
		return fmt.Errorf("write %s: %w", filePath, err)
	}
	if err := f.Chmod(recordingFilePerm); err != nil {
		return fmt.Errorf("chmod %s: %w", filePath, err)
	}
	return nil
}

func ComputeBillingInvoice(payload *MeteringPayload) BillingInvoice {
	duration := payload.WindowEnd.Sub(payload.WindowStart)
	duration = max(duration, 0)
	billable := isBillableStatus(payload.Status)
	billableSecs := int64(0)
	if billable {
		billableSecs = int64(duration / time.Second)
	}
	invoiceTotal := float64(payload.TotalPurchasedVCPU) * payload.UnitPricePerVCPUHour * (float64(billableSecs) / 3600.0)

	return BillingInvoice{
		ClusterID:            payload.ClusterID,
		Status:               payload.Status,
		WindowStart:          payload.WindowStart,
		WindowEnd:            payload.WindowEnd,
		Billable:             billable,
		BillableDurationSecs: billableSecs,
		TotalPurchasedVCPU:   payload.TotalPurchasedVCPU,
		UnitPricePerVCPUHour: payload.UnitPricePerVCPUHour,
		InvoiceTotal:         invoiceTotal,
	}
}

func isBillableStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "active", "modifying", "deleting":
		return true
	default:
		return false
	}
}

func decodeJSONL[T any](filePath string) ([]T, error) {
	f, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var out []T
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		var item T
		if err := json.Unmarshal([]byte(line), &item); err != nil {
			return nil, fmt.Errorf("parse %s: %w", filePath, err)
		}
		out = append(out, item)
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("read %s: %w", filePath, err)
	}
	return out, nil
}

// GenerateRecordingStub generates the metering / billing recorder stub code.
func GenerateRecordingStub(cfg *Config) (map[string]string, error) {
	httpPort := cfg.HTTPPort
	if httpPort == 0 {
		httpPort = DefaultHTTPPort
	}
	grpcPort := cfg.GRPCPort
	if grpcPort == 0 {
		grpcPort = DefaultGRPCPort
	}

	data := recordingTemplateData{
		Service:    cfg.Service,
		Kind:       cfg.Kind,
		HTTPPort:   httpPort,
		GRPCPort:   grpcPort,
		RecordFile: RecordingFileName(cfg.Kind),
	}

	var b strings.Builder
	if err := recordingMainTmpl.Execute(&b, data); err != nil {
		return nil, fmt.Errorf("execute recording template: %w", err)
	}

	return map[string]string{"main.go": b.String()}, nil
}

type recordingTemplateData struct {
	Service    string
	Kind       Kind
	HTTPPort   int
	GRPCPort   int
	RecordFile string
}

var recordingMainTmpl = template.Must(template.New("recording-main").Parse(`package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

type meteringPayload struct {
	ClusterID            string         ` + "`json:\"cluster_id\"`" + `
	Status               string         ` + "`json:\"status\"`" + `
	WindowStart          time.Time      ` + "`json:\"window_start\"`" + `
	WindowEnd            time.Time      ` + "`json:\"window_end\"`" + `
	VCPUByComponent      map[string]int ` + "`json:\"vcpu_by_component\"`" + `
	TotalPurchasedVCPU   int            ` + "`json:\"total_purchased_vcpu\"`" + `
	UnitPricePerVCPUHour float64        ` + "`json:\"unit_price_per_vcpu_hour,omitempty\"`" + `
}

type billingInvoice struct {
	ClusterID            string    ` + "`json:\"cluster_id\"`" + `
	Status               string    ` + "`json:\"status\"`" + `
	WindowStart          time.Time ` + "`json:\"window_start\"`" + `
	WindowEnd            time.Time ` + "`json:\"window_end\"`" + `
	Billable             bool      ` + "`json:\"billable\"`" + `
	BillableDurationSecs int64     ` + "`json:\"billable_duration_secs\"`" + `
	TotalPurchasedVCPU   int       ` + "`json:\"total_purchased_vcpu\"`" + `
	UnitPricePerVCPUHour float64   ` + "`json:\"unit_price_per_vcpu_hour\"`" + `
	InvoiceTotal         float64   ` + "`json:\"invoice_total\"`" + `
}

func main() {
	recordFile := strings.TrimSpace(os.Getenv("CP9_RECORD_FILE"))
	if recordFile == "" {
		log.Fatal("CP9_RECORD_FILE is required")
	}
{{- if eq .Kind "metering-recorder" }}
	billingEndpoint := strings.TrimSpace(os.Getenv("CP9_BILLING_ENDPOINT"))
{{- end }}

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, ` + "`" + `{}` + "`" + `)
	})
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, ` + "`" + `{"status":"ok"}` + "`" + `)
	})

{{- if eq .Kind "metering-recorder" }}
	mux.HandleFunc("POST /recording/payloads", func(w http.ResponseWriter, r *http.Request) {
		var payload meteringPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "decode payload: "+err.Error(), http.StatusBadRequest)
			return
		}
		if err := appendJSONL(recordFile, payload); err != nil {
			http.Error(w, "record payload: "+err.Error(), http.StatusInternalServerError)
			return
		}
		if billingEndpoint != "" {
			if err := postJSON(r.Context(), billingEndpoint, payload); err != nil {
				http.Error(w, "forward to billing: "+err.Error(), http.StatusBadGateway)
				return
			}
		}
		w.WriteHeader(http.StatusAccepted)
		if err := json.NewEncoder(w).Encode(payload); err != nil {
			log.Printf("encode metering response: %v", err)
		}
	})
{{- end }}

{{- if eq .Kind "billing-recorder" }}
	mux.HandleFunc("POST /api/v1/billing/tenants/{tenant_id}/billing_action_check", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		if err := json.NewEncoder(w).Encode(map[string]any{"pass": true}); err != nil {
			log.Printf("encode billing action check response: %v", err)
		}
	})

	mux.HandleFunc("POST /recording/metering", func(w http.ResponseWriter, r *http.Request) {
		var payload meteringPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "decode payload: "+err.Error(), http.StatusBadRequest)
			return
		}
		invoice := computeBillingInvoice(payload)
		if err := appendJSONL(recordFile, invoice); err != nil {
			http.Error(w, "record invoice: "+err.Error(), http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusAccepted)
		if err := json.NewEncoder(w).Encode(invoice); err != nil {
			log.Printf("encode billing response: %v", err)
		}
	})
{{- end }}

	httpAddr := fmt.Sprintf(":%d", {{.HTTPPort}})
	grpcAddr := fmt.Sprintf(":%d", {{.GRPCPort}})

	grpcServer := grpc.NewServer(grpc.UnknownServiceHandler(func(_ any, stream grpc.ServerStream) error {
		var req emptypb.Empty
		if err := stream.RecvMsg(&req); err != nil && err != io.EOF {
			return status.Errorf(codes.Internal, "decode: %v", err)
		}
		return stream.SendMsg(&emptypb.Empty{})
	}))

	lis, err := net.Listen("tcp", grpcAddr)
	if err != nil {
		log.Fatalf("failed to listen on %s: %v", grpcAddr, err)
	}

	errCh := make(chan error, 2)
	go func() {
		errCh <- http.ListenAndServe(httpAddr, mux)
	}()
	go func() {
		errCh <- grpcServer.Serve(lis)
	}()

	log.Printf("{{.Service}} {{.Kind}} stub listening on http=%s grpc=%s file=%s", httpAddr, grpcAddr, recordFile)
	if err := <-errCh; err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

func appendJSONL(path string, value any) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("mkdir %s: %w", filepath.Dir(path), err)
	}
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
	if err != nil {
		return fmt.Errorf("open %s: %w", path, err)
	}
	defer f.Close()

	data, err := json.Marshal(value)
	if err != nil {
		return fmt.Errorf("marshal %s: %w", path, err)
	}
	if _, err := f.Write(append(data, '\n')); err != nil {
		return fmt.Errorf("write %s: %w", path, err)
	}
	if err := f.Chmod(0o644); err != nil {
		return fmt.Errorf("chmod %s: %w", path, err)
	}
	return nil
}

func postJSON(ctx context.Context, url string, value any) error {
	data, err := json.Marshal(value)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(data))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}
	return nil
}

func computeBillingInvoice(payload meteringPayload) billingInvoice {
	duration := payload.WindowEnd.Sub(payload.WindowStart)
	if duration < 0 {
		duration = 0
	}
	billable := isBillableStatus(payload.Status)
	billableSecs := int64(0)
	if billable {
		billableSecs = int64(duration / time.Second)
	}
	total := float64(payload.TotalPurchasedVCPU) * payload.UnitPricePerVCPUHour * (float64(billableSecs) / 3600.0)
	return billingInvoice{
		ClusterID:            payload.ClusterID,
		Status:               payload.Status,
		WindowStart:          payload.WindowStart,
		WindowEnd:            payload.WindowEnd,
		Billable:             billable,
		BillableDurationSecs: billableSecs,
		TotalPurchasedVCPU:   payload.TotalPurchasedVCPU,
		UnitPricePerVCPUHour: payload.UnitPricePerVCPUHour,
		InvoiceTotal:         total,
	}
}

func isBillableStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "active", "modifying", "deleting":
		return true
	default:
		return false
	}
}
`))
