// Package ceph provides functionality for collecting Ceph cluster status
// directly from the local system using the ceph CLI.
package ceph

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"strings"
	"time"
)

// ClusterStatus represents the complete Ceph cluster status as collected by the agent.
type ClusterStatus struct {
	FSID          string         `json:"fsid"`
	Health        HealthStatus   `json:"health"`
	MonMap        MonitorMap     `json:"monMap,omitempty"`
	MgrMap        ManagerMap     `json:"mgrMap,omitempty"`
	OSDMap        OSDMap         `json:"osdMap"`
	PGMap         PGMap          `json:"pgMap"`
	Pools         []Pool         `json:"pools,omitempty"`
	Services      []ServiceInfo  `json:"services,omitempty"`
	CollectedAt   time.Time      `json:"collectedAt"`
}

// HealthStatus represents Ceph cluster health.
type HealthStatus struct {
	Status   string              `json:"status"` // HEALTH_OK, HEALTH_WARN, HEALTH_ERR
	Checks   map[string]Check    `json:"checks,omitempty"`
	Summary  []HealthSummary     `json:"summary,omitempty"`
}

// Check represents a health check detail.
type Check struct {
	Severity string   `json:"severity"`
	Message  string   `json:"message,omitempty"`
	Detail   []string `json:"detail,omitempty"`
}

// HealthSummary represents a health summary message.
type HealthSummary struct {
	Severity string `json:"severity"`
	Message  string `json:"message"`
}

// MonitorMap represents Ceph monitor information.
type MonitorMap struct {
	Epoch    int       `json:"epoch"`
	NumMons  int       `json:"numMons"`
	Monitors []Monitor `json:"monitors,omitempty"`
}

// Monitor represents a single Ceph monitor.
type Monitor struct {
	Name   string `json:"name"`
	Rank   int    `json:"rank"`
	Addr   string `json:"addr,omitempty"`
	Status string `json:"status,omitempty"`
}

// ManagerMap represents Ceph manager information.
type ManagerMap struct {
	Available bool   `json:"available"`
	NumMgrs   int    `json:"numMgrs"`
	ActiveMgr string `json:"activeMgr,omitempty"`
	Standbys  int    `json:"standbys"`
}

// OSDMap represents OSD status summary.
type OSDMap struct {
	Epoch    int `json:"epoch"`
	NumOSDs  int `json:"numOsds"`
	NumUp    int `json:"numUp"`
	NumIn    int `json:"numIn"`
	NumDown  int `json:"numDown,omitempty"`
	NumOut   int `json:"numOut,omitempty"`
}

// PGMap represents placement group statistics.
type PGMap struct {
	NumPGs         int     `json:"numPgs"`
	BytesTotal     uint64  `json:"bytesTotal"`
	BytesUsed      uint64  `json:"bytesUsed"`
	BytesAvailable uint64  `json:"bytesAvailable"`
	DataBytes      uint64  `json:"dataBytes,omitempty"`
	UsagePercent   float64 `json:"usagePercent"`
	DegradedRatio  float64 `json:"degradedRatio,omitempty"`
	MisplacedRatio float64 `json:"misplacedRatio,omitempty"`
	ReadBytesPerSec  uint64 `json:"readBytesPerSec,omitempty"`
	WriteBytesPerSec uint64 `json:"writeBytesPerSec,omitempty"`
	ReadOpsPerSec    uint64 `json:"readOpsPerSec,omitempty"`
	WriteOpsPerSec   uint64 `json:"writeOpsPerSec,omitempty"`
}

// Pool represents a Ceph pool.
type Pool struct {
	ID             int     `json:"id"`
	Name           string  `json:"name"`
	BytesUsed      uint64  `json:"bytesUsed"`
	BytesAvailable uint64  `json:"bytesAvailable"`
	Objects        uint64  `json:"objects"`
	PercentUsed    float64 `json:"percentUsed"`
}

// ServiceInfo represents a Ceph service summary.
type ServiceInfo struct {
	Type     string `json:"type"` // mon, mgr, osd, mds, rgw
	Running  int    `json:"running"`
	Total    int    `json:"total"`
	Daemons  []string `json:"daemons,omitempty"`
}

// IsAvailable checks if the ceph CLI is available on the system.
func IsAvailable(ctx context.Context) bool {
	cmd := exec.CommandContext(ctx, "which", "ceph")
	return cmd.Run() == nil
}

// Collect gathers Ceph cluster status using the ceph CLI.
// Returns nil if Ceph is not available or not configured on this host.
func Collect(ctx context.Context) (*ClusterStatus, error) {
	// Check if ceph CLI is available
	if !IsAvailable(ctx) {
		return nil, nil
	}

	// Try to get ceph status - this will fail if not a ceph node
	statusJSON, err := runCephCommand(ctx, "status", "--format", "json")
	if err != nil {
		// Not an error - just means this isn't a Ceph node
		return nil, nil
	}

	status, err := parseStatus(statusJSON)
	if err != nil {
		return nil, fmt.Errorf("parse ceph status: %w", err)
	}

	// Get pool usage from ceph df
	dfJSON, err := runCephCommand(ctx, "df", "--format", "json")
	if err == nil {
		pools, usagePercent, err := parseDF(dfJSON)
		if err == nil {
			status.Pools = pools
			if status.PGMap.UsagePercent == 0 && usagePercent > 0 {
				status.PGMap.UsagePercent = usagePercent
			}
		}
	}

	status.CollectedAt = time.Now().UTC()
	return status, nil
}

// runCephCommand executes a ceph command and returns the output.
func runCephCommand(ctx context.Context, args ...string) ([]byte, error) {
	// Use a reasonable timeout for each command
	cmdCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	cmd := exec.CommandContext(cmdCtx, "ceph", args...)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("ceph %s failed: %w (stderr: %s)", 
			strings.Join(args, " "), err, stderr.String())
	}

	return stdout.Bytes(), nil
}

// parseStatus parses the output of `ceph status --format json`.
func parseStatus(data []byte) (*ClusterStatus, error) {
	var raw struct {
		FSID   string `json:"fsid"`
		Health struct {
			Status  string `json:"status"`
			Checks  map[string]struct {
				Severity string `json:"severity"`
				Summary  struct {
					Message string `json:"message"`
				} `json:"summary"`
				Detail []struct {
					Message string `json:"message"`
				} `json:"detail"`
			} `json:"checks"`
		} `json:"health"`
		MonMap struct {
			Epoch int `json:"epoch"`
			Mons  []struct {
				Name string `json:"name"`
				Rank int    `json:"rank"`
				Addr string `json:"addr"`
			} `json:"mons"`
		} `json:"monmap"`
		MgrMap struct {
			Available bool   `json:"available"`
			NumActive int    `json:"num_active_name,omitempty"` 
			ActiveName string `json:"active_name"`
			Standbys  []struct {
				Name string `json:"name"`
			} `json:"standbys"`
		} `json:"mgrmap"`
		OSDMap struct {
			Epoch  int `json:"epoch"`
			NumOSD int `json:"num_osds"`
			NumUp  int `json:"num_up_osds"`
			NumIn  int `json:"num_in_osds"`
		} `json:"osdmap"`
		PGMap struct {
			NumPGs     int     `json:"num_pgs"`
			BytesTotal uint64  `json:"bytes_total"`
			BytesUsed  uint64  `json:"bytes_used"`
			BytesAvail uint64  `json:"bytes_avail"`
			DataBytes  uint64  `json:"data_bytes"`
			DegradedRatio float64 `json:"degraded_ratio"`
			MisplacedRatio float64 `json:"misplaced_ratio"`
			ReadBytesPerSec  uint64 `json:"read_bytes_sec"`
			WriteBytesPerSec uint64 `json:"write_bytes_sec"`
			ReadOpsPerSec    uint64 `json:"read_op_per_sec"`
			WriteOpsPerSec   uint64 `json:"write_op_per_sec"`
		} `json:"pgmap"`
	}

	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("unmarshal: %w", err)
	}

	status := &ClusterStatus{
		FSID: raw.FSID,
		Health: HealthStatus{
			Status: raw.Health.Status,
			Checks: make(map[string]Check),
		},
		MonMap: MonitorMap{
			Epoch:   raw.MonMap.Epoch,
			NumMons: len(raw.MonMap.Mons),
		},
		MgrMap: ManagerMap{
			Available: raw.MgrMap.Available,
			NumMgrs:   1 + len(raw.MgrMap.Standbys),
			ActiveMgr: raw.MgrMap.ActiveName,
			Standbys:  len(raw.MgrMap.Standbys),
		},
		OSDMap: OSDMap{
			Epoch:   raw.OSDMap.Epoch,
			NumOSDs: raw.OSDMap.NumOSD,
			NumUp:   raw.OSDMap.NumUp,
			NumIn:   raw.OSDMap.NumIn,
			NumDown: raw.OSDMap.NumOSD - raw.OSDMap.NumUp,
			NumOut:  raw.OSDMap.NumOSD - raw.OSDMap.NumIn,
		},
		PGMap: PGMap{
			NumPGs:         raw.PGMap.NumPGs,
			BytesTotal:     raw.PGMap.BytesTotal,
			BytesUsed:      raw.PGMap.BytesUsed,
			BytesAvailable: raw.PGMap.BytesAvail,
			DataBytes:      raw.PGMap.DataBytes,
			DegradedRatio:  raw.PGMap.DegradedRatio,
			MisplacedRatio: raw.PGMap.MisplacedRatio,
			ReadBytesPerSec:  raw.PGMap.ReadBytesPerSec,
			WriteBytesPerSec: raw.PGMap.WriteBytesPerSec,
			ReadOpsPerSec:    raw.PGMap.ReadOpsPerSec,
			WriteOpsPerSec:   raw.PGMap.WriteOpsPerSec,
		},
	}

	// Calculate usage percent
	if raw.PGMap.BytesTotal > 0 {
		status.PGMap.UsagePercent = float64(raw.PGMap.BytesUsed) / float64(raw.PGMap.BytesTotal) * 100
	}

	// Parse monitors
	for _, mon := range raw.MonMap.Mons {
		status.MonMap.Monitors = append(status.MonMap.Monitors, Monitor{
			Name: mon.Name,
			Rank: mon.Rank,
			Addr: mon.Addr,
		})
	}

	// Parse health checks
	for name, check := range raw.Health.Checks {
		details := make([]string, 0, len(check.Detail))
		for _, d := range check.Detail {
			details = append(details, d.Message)
		}
		status.Health.Checks[name] = Check{
			Severity: check.Severity,
			Message:  check.Summary.Message,
			Detail:   details,
		}
	}

	// Build service summary
	status.Services = []ServiceInfo{
		{Type: "mon", Running: len(raw.MonMap.Mons), Total: len(raw.MonMap.Mons)},
		{Type: "mgr", Running: boolToInt(raw.MgrMap.Available), Total: status.MgrMap.NumMgrs},
		{Type: "osd", Running: raw.OSDMap.NumUp, Total: raw.OSDMap.NumOSD},
	}

	return status, nil
}

// parseDF parses the output of `ceph df --format json`.
func parseDF(data []byte) ([]Pool, float64, error) {
	var raw struct {
		Stats struct {
			TotalBytes     uint64  `json:"total_bytes"`
			TotalUsedBytes uint64  `json:"total_used_bytes"`
			PercentUsed    float64 `json:"percent_used"`
		} `json:"stats"`
		Pools []struct {
			ID    int    `json:"id"`
			Name  string `json:"name"`
			Stats struct {
				BytesUsed   uint64  `json:"bytes_used"`
				MaxAvail    uint64  `json:"max_avail"`
				Objects     uint64  `json:"objects"`
				PercentUsed float64 `json:"percent_used"`
			} `json:"stats"`
		} `json:"pools"`
	}

	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, 0, fmt.Errorf("unmarshal df: %w", err)
	}

	pools := make([]Pool, 0, len(raw.Pools))
	for _, p := range raw.Pools {
		pools = append(pools, Pool{
			ID:             p.ID,
			Name:           p.Name,
			BytesUsed:      p.Stats.BytesUsed,
			BytesAvailable: p.Stats.MaxAvail,
			Objects:        p.Stats.Objects,
			PercentUsed:    p.Stats.PercentUsed * 100, // Convert from 0-1 to 0-100
		})
	}

	return pools, raw.Stats.PercentUsed * 100, nil
}

func boolToInt(b bool) int {
	if b {
		return 1
	}
	return 0
}
