package status

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode"
)

var (
	dailyLimitRegex  = regexp.MustCompile(`5h limit:.*?\]\s*([^%\n]+%)[^\(]*\(resets\s+([^\)]+)\)`)
	weeklyLimitRegex = regexp.MustCompile(`Weekly limit:.*?\]\s*([^%\n]+%)[^\(]*\(resets\s+([^\)]+)\)`)

	// Section header locators — they only locate the start of each block; they
	// do NOT span forward into other blocks. This prevents the previous lazy
	// `.*?` regex from accidentally pairing a session header with the week's
	// "X% used / Resets Y" payload when cursor positioning in the raw PTY
	// stream interleaves the two sections out of order.
	claudeSessionHeaderRegex = regexp.MustCompile(`(?i)Current\s+session\b`)
	claudeWeekHeaderRegex    = regexp.MustCompile(`(?i)Current\s+week\b(?:\s*\(all\s+models\))?`)

	// Payload extractor used *within* a single section slice.
	claudePayloadRegex = regexp.MustCompile(`(?is)(\d+(?:\.\d+)?)%\s+used.*?Resets\s+([^\n]+)`)

	antigravityUsedQuotaRegex    = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)%\s+used\s+\(Limit resets in ([^)]+)\)`)
	antigravitySimpleQuotaRegex  = regexp.MustCompile(`(?is)\bquota\s+(\d+(?:\.\d+)?)%\s+used\b`)
	antigravityLimitReachedRegex = regexp.MustCompile(`(?i)Limit reached,\s*resets in ([^\n]+)`)
	antigravityUsageLimitRegex   = regexp.MustCompile(`(?i)Usage limit:\s*([0-9][0-9,]*)`)
	antigravityModelLineRegex    = regexp.MustCompile(`(?i)^(.*)\s+(\d+(?:\.\d+)?)%\s+Resets:\s*(.*)$`)
)

// ParseStatusOutput attempts to parse Claude or Codex usage limits from a raw
// PTY stream. Claude's /usage TUI uses cursor positioning and erase sequences
// to repaint the same panel multiple times, so the stream-append order can
// differ from what is actually visible on screen. We render the raw bytes
// through a terminal screen model first (renderVisibleScreen) and try Claude
// parsing against that; if it fails or only partially succeeds we fall back to
// the line-oriented cleanANSI which handles the simpler Codex output and
// non-cursor-repainted Claude streams.
func ParseStatusOutput(raw string) (*UsageStatus, error) {
	visible := renderVisibleScreen(raw, 60, 200)
	cleanRaw := cleanANSI(raw)

	// Prefer whichever input yields a more complete Claude parse: a full
	// session+week match beats a partial one, which beats nothing.
	visStatus := &UsageStatus{RawOutput: visible}
	rawStatus := &UsageStatus{RawOutput: cleanRaw}
	visStatus.parseClaudeUsage(visible)
	rawStatus.parseClaudeUsage(cleanRaw)

	if pick := pickMoreComplete(visStatus, rawStatus); pick != nil {
		return pick, nil
	}
	if rawStatus.parseClaudeStatus(cleanRaw) || rawStatus.parseClaudeStatus(visible) {
		return rawStatus, nil
	}

	// Neither input produced Claude data — try Antigravity, then Codex.
	parsedAntigravity := rawStatus.parseAntigravityQuota(cleanRaw)
	if !rawStatus.parseAntigravityModelUsage(cleanRaw) {
		rawStatus.parseAntigravityModelUsage(visible)
	}
	if parsedAntigravity || rawStatus.Metadata["model_usage_count"] != "" {
		return rawStatus, nil
	}
	rawStatus.parseCodexUsage(cleanRaw)
	return rawStatus, nil
}

// parseAntigravityQuota parses Antigravity `/usage` daily quota output.
// `0% used (Limit resets in 24h)` and `Usage limit: 200` populate
// DailyLimit (remaining percent), DailyResetTime, and metadata. Real
// screen-reader output may also expose the header as `quota` followed by
// `0% used` without reset or limit details; in that case only remaining and
// used percentages are populated.
// `Limit reached, resets in 5h` is treated as 100% used.
func (s *UsageStatus) parseAntigravityQuota(cleanRaw string) bool {
	used := ""
	reset := ""
	if m := antigravityUsedQuotaRegex.FindStringSubmatch(cleanRaw); len(m) >= 3 {
		used = m[1] + "%"
		reset = strings.TrimSpace(m[2])
	} else if m := antigravityLimitReachedRegex.FindStringSubmatch(cleanRaw); len(m) >= 2 {
		used = "100%"
		reset = strings.TrimSpace(m[1])
	} else if m := antigravitySimpleQuotaRegex.FindStringSubmatch(cleanRaw); len(m) >= 2 {
		used = m[1] + "%"
	}

	limitNumeric := ""
	if m := antigravityUsageLimitRegex.FindStringSubmatch(cleanRaw); len(m) >= 2 {
		limitNumeric = strings.TrimSpace(m[1])
	}

	if used == "" {
		return false
	}

	if s.Metadata == nil {
		s.Metadata = make(map[string]string)
	}
	s.Metadata["daily_label"] = "Daily quota"
	s.DailyLimit = remainingPercent(used)
	s.DailyResetTime = reset
	s.Metadata["used_percent"] = used
	if limitNumeric != "" {
		s.Metadata["usage_limit"] = limitNumeric
	}
	return true
}

// parseAntigravityModelUsage parses the `/model` screen-reader "Model usage" area,
// storing per-model used percentages and reset times in metadata.
func (s *UsageStatus) parseAntigravityModelUsage(cleanRaw string) bool {
	lower := strings.ToLower(cleanRaw)
	start := strings.LastIndex(lower, "model usage")
	if start < 0 {
		return false
	}
	block := cleanRaw[start+len("model usage"):]
	blockLower := strings.ToLower(block)
	if end := strings.Index(blockLower, "(press esc"); end >= 0 {
		block = block[:end]
	}

	type modelUsage struct {
		name        string
		usedPercent string
		reset       string
	}

	var models []modelUsage
	modelIndex := make(map[string]int)
	lines := strings.Split(block, "\n")
	for i := 0; i < len(lines); i++ {
		line := strings.TrimSpace(lines[i])
		if line == "" || !strings.Contains(line, "%") || !strings.Contains(strings.ToLower(line), "resets:") {
			continue
		}

		m := antigravityModelLineRegex.FindStringSubmatch(line)
		if len(m) < 4 {
			continue
		}

		name := antigravityModelNameFromUsagePrefix(m[1])
		if name == "" {
			continue
		}
		used := strings.TrimSpace(m[2]) + "%"
		reset := strings.TrimSpace(m[3])
		if !strings.Contains(reset, ")") && i+1 < len(lines) {
			next := strings.TrimSpace(lines[i+1])
			if strings.HasPrefix(next, "(") {
				reset = strings.TrimSpace(reset + " " + next)
				i++
			}
		}

		entry := modelUsage{name: name, usedPercent: used, reset: reset}
		if idx, ok := modelIndex[name]; ok {
			models[idx] = entry
			continue
		}
		modelIndex[name] = len(models)
		models = append(models, entry)
	}

	if len(models) == 0 {
		return false
	}

	if s.Metadata == nil {
		s.Metadata = make(map[string]string)
	}
	s.Metadata["model_usage_count"] = strconv.Itoa(len(models))
	for i, model := range models {
		prefix := fmt.Sprintf("model_usage_%d", i)
		s.Metadata[prefix+"_name"] = model.name
		s.Metadata[prefix+"_used_percent"] = model.usedPercent
		if model.reset != "" {
			s.Metadata[prefix+"_reset"] = model.reset
		}
	}
	return true
}

func antigravityModelNameFromUsagePrefix(prefix string) string {
	prefix = strings.TrimSpace(prefix)
	if prefix == "" {
		return ""
	}
	if idx := strings.IndexAny(prefix, "▬━─█░#▉▊▋▌▍▎▏■"); idx >= 0 {
		return strings.TrimSpace(prefix[:idx])
	}
	return prefix
}

// pickMoreComplete returns the status with more fields populated, or nil if
// neither has any Claude limit data.
func pickMoreComplete(a, b *UsageStatus) *UsageStatus {
	score := func(s *UsageStatus) int {
		n := 0
		if s.DailyLimit != "" {
			n++
		}
		if s.WeeklyLimit != "" {
			n++
		}
		if s.DailyResetTime != "" {
			n++
		}
		if s.WeeklyResetTime != "" {
			n++
		}
		return n
	}
	sa, sb := score(a), score(b)
	if sa == 0 && sb == 0 {
		return nil
	}
	if sa >= sb {
		return a
	}
	return b
}

// parseClaudeUsage attempts to parse Claude /usage output format.
//
// We locate each section header (Current session / Current week) and extract
// the "X% used … Resets Y" payload that immediately follows it, stopping at
// the next opposing header so the session header is never paired with the
// week's payload. Each header may appear multiple times in the raw stream
// (initial loading repaint then data repaint); we walk the occurrences from
// last to first and take the first one whose section contains a payload, so
// the most recent render wins.
func (s *UsageStatus) parseClaudeUsage(cleanRaw string) bool {
	sessionLocs := claudeSessionHeaderRegex.FindAllStringIndex(cleanRaw, -1)
	weekLocs := claudeWeekHeaderRegex.FindAllStringIndex(cleanRaw, -1)

	extract := func(headerLocs [][]int, oppositeLocs [][]int) (string, string, bool) {
		for i := len(headerLocs) - 1; i >= 0; i-- {
			start := headerLocs[i][1]
			end := len(cleanRaw)
			for _, opp := range oppositeLocs {
				if opp[0] > start && opp[0] < end {
					end = opp[0]
				}
			}
			if end <= start {
				continue
			}
			section := cleanRaw[start:end]
			if m := claudePayloadRegex.FindStringSubmatch(section); len(m) >= 3 {
				return m[1], strings.TrimSpace(m[2]), true
			}
		}
		return "", "", false
	}

	if used, reset, ok := extract(sessionLocs, weekLocs); ok {
		if s.Metadata == nil {
			s.Metadata = make(map[string]string)
		}
		s.Metadata["daily_label"] = "Current session"
		s.DailyLimit = remainingPercent(used)
		s.DailyResetTime = reset
	}
	if used, reset, ok := extract(weekLocs, sessionLocs); ok {
		if s.Metadata == nil {
			s.Metadata = make(map[string]string)
		}
		s.Metadata["weekly_label"] = "Current week"
		s.WeeklyLimit = remainingPercent(used)
		s.WeeklyResetTime = reset
	}
	return s.DailyLimit != "" || s.WeeklyLimit != ""
}

func (s *UsageStatus) parseClaudeStatus(cleanRaw string) bool {
	lower := strings.ToLower(cleanRaw)
	if !strings.Contains(lower, "claudecode") && !strings.Contains(lower, "claude code") {
		return false
	}

	fields := map[string]string{
		"version":      "claude_status_version",
		"sessionname":  "claude_status_session_name",
		"sessionid":    "claude_status_session_id",
		"cwd":          "claude_status_cwd",
		"loginmethod":  "claude_status_login_method",
		"organization": "claude_status_organization",
		"email":        "claude_status_email",
		"model":        "claude_status_model",
		"mcpservers":   "claude_status_mcp_servers",
	}

	found := 0
	for _, line := range strings.Split(cleanRaw, "\n") {
		key, value, ok := strings.Cut(line, ":")
		if !ok {
			continue
		}
		normalized := normalizeClaudeStatusKey(key)
		metadataKey, ok := fields[normalized]
		if !ok {
			continue
		}
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if s.Metadata == nil {
			s.Metadata = make(map[string]string)
		}
		s.Metadata[metadataKey] = value
		found++
	}
	if found == 0 {
		return false
	}
	s.Metadata["status_kind"] = "claude"
	return true
}

func normalizeClaudeStatusKey(key string) string {
	var b strings.Builder
	for _, r := range strings.ToLower(key) {
		if unicode.IsLetter(r) {
			b.WriteRune(r)
		}
	}
	return b.String()
}

// parseCodexUsage attempts to parse Codex output format and sets metadata labels.
func (s *UsageStatus) parseCodexUsage(cleanRaw string) {
	if match := dailyLimitRegex.FindStringSubmatch(cleanRaw); len(match) > 2 {
		s.DailyLimit = strings.TrimSpace(match[1])
		s.DailyResetTime = strings.TrimSpace(match[2])
	}
	if match := weeklyLimitRegex.FindStringSubmatch(cleanRaw); len(match) > 2 {
		s.WeeklyLimit = strings.TrimSpace(match[1])
		s.WeeklyResetTime = strings.TrimSpace(match[2])
	}
	if s.DailyLimit != "" || s.WeeklyLimit != "" {
		if s.Metadata == nil {
			s.Metadata = make(map[string]string)
		}
		if s.DailyLimit != "" {
			s.Metadata["daily_label"] = "5h limit"
		}
		if s.WeeklyLimit != "" {
			s.Metadata["weekly_label"] = "Weekly limit"
		}
	}
}

// remainingPercent converts a "used" percentage string to remaining percentage.
func remainingPercent(usedStr string) string {
	usedStr = strings.TrimSuffix(usedStr, "%")
	usedStr = strings.TrimSpace(usedStr)
	usedVal, err := strconv.ParseFloat(usedStr, 64)
	if err != nil {
		return usedStr
	}
	remaining := 100 - usedVal
	if remaining < 0 {
		remaining = 0
	}
	if usedVal == float64(int(usedVal)) {
		return fmt.Sprintf("%d%%", int(remaining))
	}
	return fmt.Sprintf("%.1f%%", remaining)
}
