package pizzint import ( "context" "encoding/json" "fmt" "log/slog" "strings" "time" "gitea.stevedudenhoeffer.com/steve/go-extractor" ) const dashboardAPIURL = "https://www.pizzint.watch/api/dashboard-data" // DoughconLevel represents the DOUGHCON threat level (modeled after DEFCON). // Lower numbers indicate higher activity. type DoughconLevel int const ( DoughconMaximum DoughconLevel = 1 // Maximum Alert DoughconHigh DoughconLevel = 2 // High Activity DoughconElevated DoughconLevel = 3 // Elevated Activity DoughconWatch DoughconLevel = 4 // Increased Intelligence Watch DoughconQuiet DoughconLevel = 5 // All Quiet ) func (d DoughconLevel) String() string { switch d { case DoughconQuiet: return "DOUGHCON 5 - ALL QUIET" case DoughconWatch: return "DOUGHCON 4 - DOUBLE TAKE" case DoughconElevated: return "DOUGHCON 3 - ELEVATED" case DoughconHigh: return "DOUGHCON 2 - HIGH ACTIVITY" case DoughconMaximum: return "DOUGHCON 1 - MAXIMUM ALERT" default: return fmt.Sprintf("DOUGHCON %d", d) } } // Label returns a short label for the DOUGHCON level. func (d DoughconLevel) Label() string { switch d { case DoughconQuiet: return "ALL QUIET" case DoughconWatch: return "DOUBLE TAKE" case DoughconElevated: return "ELEVATED" case DoughconHigh: return "HIGH ACTIVITY" case DoughconMaximum: return "MAXIMUM ALERT" default: return "UNKNOWN" } } // Restaurant represents a monitored pizza restaurant near the Pentagon. type Restaurant struct { Name string `json:"name"` CurrentPopularity int `json:"current_popularity"` PercentOfUsual *int `json:"percent_of_usual,omitempty"` IsSpike bool `json:"is_spike"` SpikeMagnitude string `json:"spike_magnitude,omitempty"` IsClosed bool `json:"is_closed"` DataFreshness string `json:"data_freshness"` } // Status returns a human-readable status string like "QUIET", "CLOSED", or "139% SPIKE". func (r Restaurant) Status() string { if r.IsClosed { return "CLOSED" } if r.IsSpike && r.PercentOfUsual != nil { return fmt.Sprintf("%d%% SPIKE", *r.PercentOfUsual) } if r.IsSpike { return "SPIKE" } return "QUIET" } // Event represents a detected spike event at a monitored location. type Event struct { Name string `json:"name"` MinutesAgo int `json:"minutes_ago"` } // PizzaStatus is the top-level result from the PizzINT dashboard. type PizzaStatus struct { DoughconLevel DoughconLevel `json:"doughcon_level"` DoughconLabel string `json:"doughcon_label"` OverallIndex int `json:"overall_index"` Restaurants []Restaurant `json:"restaurants"` Events []Event `json:"events,omitempty"` FetchedAt time.Time `json:"fetched_at"` } // Config holds configuration for the PizzINT extractor. type Config struct{} // DefaultConfig is the default PizzINT configuration. var DefaultConfig = Config{} func (c Config) validate() Config { return c } // GetStatus fetches the current pizza activity status from the PizzINT dashboard. func (c Config) GetStatus(ctx context.Context, b extractor.Browser) (*PizzaStatus, error) { c = c.validate() slog.Info("fetching pizza status", "url", dashboardAPIURL) doc, err := b.Open(ctx, dashboardAPIURL, extractor.OpenPageOptions{}) if err != nil { return nil, fmt.Errorf("failed to open pizzint API: %w", err) } defer extractor.DeferClose(doc) return extractStatus(doc) } // GetStatus is a convenience function using DefaultConfig. func GetStatus(ctx context.Context, b extractor.Browser) (*PizzaStatus, error) { return DefaultConfig.GetStatus(ctx, b) } func extractStatus(doc extractor.Document) (*PizzaStatus, error) { // The browser renders the JSON API response as text in the page body. // doc.Text() returns InnerText of the html element, which should // contain the raw JSON (possibly with extra browser UI text). body, err := doc.Text() if err != nil { return nil, fmt.Errorf("failed to get page text: %w", err) } jsonStr, err := findJSON(body) if err != nil { // Fall back to Content() which returns the full HTML — the JSON // will be embedded in it (e.g. inside a
 tag in Chromium).
		html, herr := doc.Content()
		if herr != nil {
			return nil, fmt.Errorf("failed to extract JSON from text (%w) and failed to get HTML: %w", err, herr)
		}
		jsonStr, err = findJSON(html)
		if err != nil {
			return nil, fmt.Errorf("no valid JSON found in API response: %w", err)
		}
	}

	var resp dashboardResponse
	if err := json.Unmarshal([]byte(jsonStr), &resp); err != nil {
		return nil, fmt.Errorf("failed to parse dashboard response: %w", err)
	}

	if !resp.Success {
		return nil, fmt.Errorf("API returned success=false")
	}

	return resp.toPizzaStatus(), nil
}

// findJSON extracts a JSON object from a string by matching braces.
func findJSON(s string) (string, error) {
	start := strings.Index(s, "{")
	if start == -1 {
		return "", fmt.Errorf("no opening brace found")
	}

	depth := 0
	inString := false
	escape := false

	for i := start; i < len(s); i++ {
		ch := s[i]

		if escape {
			escape = false
			continue
		}

		if ch == '\\' && inString {
			escape = true
			continue
		}

		if ch == '"' {
			inString = !inString
			continue
		}

		if inString {
			continue
		}

		switch ch {
		case '{':
			depth++
		case '}':
			depth--
			if depth == 0 {
				return s[start : i+1], nil
			}
		}
	}

	return "", fmt.Errorf("no matching closing brace found")
}

// dashboardResponse is the raw API response from /api/dashboard-data.
type dashboardResponse struct {
	Success      bool                  `json:"success"`
	OverallIndex int                   `json:"overall_index"`
	DefconLevel  int                   `json:"defcon_level"`
	Data         []dashboardRestaurant `json:"data"`
	Events       []dashboardEvent      `json:"events"`
}

type dashboardRestaurant struct {
	PlaceID           string  `json:"place_id"`
	Name              string  `json:"name"`
	Address           string  `json:"address"`
	CurrentPopularity int     `json:"current_popularity"`
	PercentageOfUsual *int    `json:"percentage_of_usual"`
	IsSpike           bool    `json:"is_spike"`
	SpikeMagnitude    *string `json:"spike_magnitude"`
	DataSource        string  `json:"data_source"`
	DataFreshness     string  `json:"data_freshness"`
	IsClosedNow       bool    `json:"is_closed_now"`
}

type dashboardEvent struct {
	Name       string `json:"name"`
	MinutesAgo int    `json:"minutes_ago"`
}

func (r dashboardResponse) toPizzaStatus() *PizzaStatus {
	status := &PizzaStatus{
		DoughconLevel: DoughconLevel(r.DefconLevel),
		OverallIndex:  r.OverallIndex,
		FetchedAt:     time.Now(),
	}
	status.DoughconLabel = status.DoughconLevel.Label()

	for _, d := range r.Data {
		rest := Restaurant{
			Name:              d.Name,
			CurrentPopularity: d.CurrentPopularity,
			PercentOfUsual:    d.PercentageOfUsual,
			IsSpike:           d.IsSpike,
			IsClosed:          d.IsClosedNow,
			DataFreshness:     d.DataFreshness,
		}
		if d.SpikeMagnitude != nil {
			rest.SpikeMagnitude = *d.SpikeMagnitude
		}
		status.Restaurants = append(status.Restaurants, rest)
	}

	for _, e := range r.Events {
		status.Events = append(status.Events, Event{
			Name:       e.Name,
			MinutesAgo: e.MinutesAgo,
		})
	}

	return status
}