<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Health;

/**
 * One line of the `shop_health` answer.
 *
 * Two invariants live here rather than in the six collectors, because they are what makes the tool
 * worth calling at all:
 *
 * 1. **A bare number is not a finding.** "41 errors" is noise; "41 errors vs 3/day" is a bug
 *    report. `baseline7d` is therefore a first-class field, not something buried in `detail`.
 * 2. **A non-green signal must hand over the next call.** `next` is a literal, runnable tool-call
 *    string (`read_logs({source:'system',level:'error'})`) — progressive disclosure, not a
 *    paragraph telling the LLM how it might go and look. {@see self::degraded()} is the only
 *    constructor that can produce an amber, and it supplies one too.
 */
final class Signal
{
    /**
     * @param string          $id         stable machine id, e.g. `system_errors`
     * @param int|float|string|null $value the measured quantity, in the unit named by $id
     * @param int|float|null  $baseline7d per-day average over the 7 days BEFORE the window, or
     *                                    null where a baseline is not meaningful (cron has no run
     *                                    history table to average over)
     * @param string|null     $detail     one sentence, only when the number alone under-explains
     * @param string|null     $next       literal tool call; required for every non-green signal
     * @param array<string,mixed> $extra  signal-specific fields (e.g. log_volume's `capacity`)
     */
    public function __construct(
        public readonly string $id,
        public readonly HealthStatus $status,
        public readonly int|float|string|null $value,
        public readonly int|float|null $baseline7d = null,
        public readonly ?string $detail = null,
        public readonly ?string $next = null,
        public readonly array $extra = [],
    ) {
    }

    /**
     * The honest-failure constructor: the source table is missing, or its query blew up.
     *
     * `shop_health` is the tool the LLM reaches for first, so one broken source must never take the
     * answer down — it degrades to amber ("I could not check this") instead of green ("nothing
     * wrong here"), which would be a lie. The `next` call is genuinely useful: `list_tables` is how
     * you find out whether the table exists at all on this install.
     */
    public static function degraded(string $id, string $table, string $reason): self
    {
        return new self(
            $id,
            HealthStatus::Amber,
            null,
            null,
            \sprintf('Not checked: %s (%s). Treat this signal as unknown, not as healthy.', $reason, $table),
            \sprintf('list_tables({pattern:\'%s\'})', $table),
        );
    }

    /**
     * @return array{id:string,status:string,value:int|float|string|null,baseline7d?:int|float,
     *     detail?:string,next?:string}&array<string,mixed>
     */
    public function toArray(): array
    {
        $out = [
            'id'     => $this->id,
            'status' => $this->status->value,
            'value'  => $this->value,
        ];
        if ($this->baseline7d !== null) {
            $out['baseline7d'] = $this->baseline7d;
        }
        if ($this->detail !== null) {
            $out['detail'] = $this->detail;
        }
        if ($this->next !== null) {
            $out['next'] = $this->next;
        }

        return $out + $this->extra;
    }
}
