<?php
/**
 * Top-level audit result data object.
 *
 * @package NimblyMail\SMTP
 */

namespace NimblyMail\SMTP\Audit;

defined( 'ABSPATH' ) || exit;

/**
 * Class AuditResult
 *
 * Snapshot of a single audit run. Holds the audited domain, the active mailer
 * slug at the time of the run, per-pillar results, the resolver path used,
 * and the composite status derived from the pillar mix.
 *
 * @since 1.0.0
 */
final class AuditResult {

    /**
     * All pillars passed (or were Skipped).
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const COMPOSITE_GREEN = 'green';

    /**
     * At least one pillar warned, none failed.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const COMPOSITE_YELLOW = 'yellow';

    /**
     * At least one pillar failed.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const COMPOSITE_RED = 'red';

    /**
     * Resolver path token written when the resolver could not answer at all.
     *
     * @since 1.0.0
     *
     * @var string
     */
    public const RESOLVER_FAILED = 'failed';

    /**
     * Unix timestamp of the audit run.
     *
     * @since 1.0.0
     *
     * @var int
     */
    private int $audited_at;

    /**
     * Domain that was audited.
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $domain;

    /**
     * Active mailer slug at the time of the audit.
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $mailer;

    /**
     * Composite status derived from per-pillar statuses.
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $composite;

    /**
     * Per-pillar results keyed by pillar id (spf|dkim|dmarc|ptr).
     *
     * @since 1.0.0
     *
     * @var array<string, PillarResult>
     */
    private array $pillars;

    /**
     * Resolver path used (native|doh-cloudflare|doh-google|failed).
     *
     * @since 1.0.0
     *
     * @var string
     */
    private string $resolver_used;

    /**
     * Whether the audited domain is on the public-mailbox blocklist.
     *
     * @since 1.0.0
     *
     * @var bool
     */
    private bool $public_domain_warning;

    /**
     * Constructor.
     *
     * @since 1.0.0
     *
     * @param int                         $audited_at            Unix timestamp.
     * @param string                      $domain                Audited domain.
     * @param string                      $mailer                Active mailer slug.
     * @param array<string, PillarResult> $pillars               Per-pillar results.
     * @param string                      $resolver_used         Resolver path used.
     * @param bool                        $public_domain_warning Whether the domain is on the public-mailbox blocklist.
     */
    public function __construct(
        int $audited_at,
        string $domain,
        string $mailer,
        array $pillars,
        string $resolver_used,
        bool $public_domain_warning
    ) {
        $this->audited_at            = $audited_at;
        $this->domain                = $domain;
        $this->mailer                = $mailer;
        $this->pillars               = $pillars;
        $this->resolver_used         = $resolver_used;
        $this->public_domain_warning = $public_domain_warning;
        $this->composite             = $this->derive_composite( $pillars );
    }

    /**
     * Compute the composite status from per-pillar results.
     *
     * Any Fail produces Red. Any Warn (without Fail) produces Yellow.
     * Otherwise Green.
     *
     * @since 1.0.0
     *
     * @param array<string, PillarResult> $pillars Per-pillar results.
     *
     * @return string Composite status constant.
     */
    private function derive_composite( array $pillars ): string {
        $has_warn = false;

        foreach ( $pillars as $pillar ) {
            if ( PillarResult::STATUS_FAIL === $pillar->get_status() ) {
                return self::COMPOSITE_RED;
            }
            if ( PillarResult::STATUS_WARN === $pillar->get_status() ) {
                $has_warn = true;
            }
        }

        return $has_warn ? self::COMPOSITE_YELLOW : self::COMPOSITE_GREEN;
    }

    /**
     * Get the unix timestamp of the audit run.
     *
     * @since 1.0.0
     *
     * @return int
     */
    public function get_audited_at(): int {
        return $this->audited_at;
    }

    /**
     * Get the audited domain.
     *
     * @since 1.0.0
     *
     * @return string
     */
    public function get_domain(): string {
        return $this->domain;
    }

    /**
     * Get the active mailer slug at audit time.
     *
     * @since 1.0.0
     *
     * @return string
     */
    public function get_mailer(): string {
        return $this->mailer;
    }

    /**
     * Get the composite status.
     *
     * @since 1.0.0
     *
     * @return string
     */
    public function get_composite(): string {
        return $this->composite;
    }

    /**
     * Get all pillars keyed by id.
     *
     * @since 1.0.0
     *
     * @return array<string, PillarResult>
     */
    public function get_pillars(): array {
        return $this->pillars;
    }

    /**
     * Get a single pillar by id, or null when absent.
     *
     * @since 1.0.0
     *
     * @param string $id Pillar id.
     *
     * @return PillarResult|null
     */
    public function get_pillar( string $id ): ?PillarResult {
        return $this->pillars[ $id ] ?? null;
    }

    /**
     * Get the resolver path used.
     *
     * @since 1.0.0
     *
     * @return string
     */
    public function get_resolver_used(): string {
        return $this->resolver_used;
    }

    /**
     * Whether the audited domain is on the public-mailbox blocklist.
     *
     * @since 1.0.0
     *
     * @return bool
     */
    public function has_public_domain_warning(): bool {
        return $this->public_domain_warning;
    }

    /**
     * Serialize to an array suitable for option storage and REST responses.
     *
     * @since 1.0.0
     *
     * @return array
     */
    public function to_array(): array {
        $pillars = array();
        foreach ( $this->pillars as $id => $pillar ) {
            $pillars[ $id ] = $pillar->to_array();
        }

        return array(
            'audited_at'            => $this->audited_at,
            'domain'                => $this->domain,
            'mailer'                => $this->mailer,
            'composite'             => $this->composite,
            'pillars'               => $pillars,
            'resolver_used'         => $this->resolver_used,
            'public_domain_warning' => $this->public_domain_warning,
        );
    }

    /**
     * Hydrate from an array (inverse of to_array). Returns null on malformed input.
     *
     * @since 1.0.0
     *
     * @param array $data Serialized snapshot.
     *
     * @return self|null
     */
    public static function from_array( array $data ): ?self {
        if ( ! isset( $data['audited_at'], $data['domain'], $data['mailer'], $data['pillars'] ) ) {
            return null;
        }

        if ( ! is_array( $data['pillars'] ) ) {
            return null;
        }

        $pillars = array();
        foreach ( $data['pillars'] as $id => $pillar_data ) {
            if ( is_array( $pillar_data ) ) {
                $pillars[ (string) $id ] = PillarResult::from_array( $pillar_data );
            }
        }

        return new self(
            (int) $data['audited_at'],
            (string) $data['domain'],
            (string) $data['mailer'],
            $pillars,
            isset( $data['resolver_used'] ) ? (string) $data['resolver_used'] : self::RESOLVER_FAILED,
            ! empty( $data['public_domain_warning'] )
        );
    }
}
