/**
 * Dashboard page prop types — extracted from Dashboard.tsx (DEBT-005).
 *
 * Keep in sync with DashboardController::index(). The controller is the
 * source of truth; if you add a prop there, add it here.
 */
import type { ServiceHealth } from '@/Components/CircuitBreaker/ConnectionHealthPanel';
import type { AlgorithmCorrelation } from '@/Components/Dashboard/AlgorithmCorrelationCard';
import type { PositionDeltaQuery } from '@/Components/TrackedQueries/PositionDeltaStrip';
import type { PageProps } from '@/types';

export interface FunnelMetrics {
  generated: number;
  approved: number;
  applied: number;
}

export interface TopDecliningPage {
  id: number;
  page_url: string;
  delta_percent: number;
  delta_absolute: number;
}

export interface DashboardMetrics {
  total_clicks_28d: number;
  total_clicks_28d_prior: number;
  /**
   * Period-over-period delta in percent (current 28d vs prior 28d).
   * `null` means the prior period had zero clicks — render "—" instead of
   * a misleading "+∞%" or "+0%".
   */
  total_clicks_28d_delta_percent: number | null;
  pending_recommendations: number;
  drafts_generated_month: number;
  top_declining_pages: { data: TopDecliningPage[] };
  latest_analysis_at: string | null;
}

export interface GscExpiringConnection {
  site_id: string;
  site_name: string;
  expires_at: string;
  days_until_expiry: number;
}

export interface ContentIntelligence {
  /**
   * The site these counts are scoped to. Always equal to first_site for now.
   * Surfaced so the UI can disambiguate scope on multi-site accounts —
   * metrics above are aggregated, these are first-site only.
   * Note: emitted as public_id (ULID string) per HasPublicId serialization invariant.
   */
  site_id: string;
  site_name: string;
  cannibalization_count: number;
  topic_cluster_count: number;
  freshness_count: number;
  keyword_opportunity_count: number;
}

export interface UserActivity {
  analyses_this_week: number;
  recommendations_total: number;
  recommendations_applied: number;
  recommendations_pending: number;
}

export interface DashboardPageProps extends PageProps {
  funnel_metrics: FunnelMetrics;
  dashboard_metrics: DashboardMetrics;
  health_score: {
    score: number | null;
    previous_score: number | null;
    status: string;
    trend: string;
    calculated_at: string | null;
    components: {
      traffic_trend: number | null;
      content_quality: number | null;
      recommendation_completion: number | null;
      roi_performance: number | null;
      connection_health: number | null;
    };
  };
  wizard_completed?: boolean;
  onboarding_feature_enabled?: boolean;
  is_activated?: boolean;
  has_reviewed_recommendation?: boolean;
  /** True once the user has generated their first AI rewrite draft (deep_activated_at set). */
  has_generated_draft?: boolean;
  setup_dismissed?: boolean;
  force_show_next_step?: string | null;
  gsc_connected_no_run?: boolean;
  step2_banner_dismissed?: boolean;
  gsc_syncing?: boolean;
  // Always present: DashboardController redirects to sites.create when the user
  // has no owned or member site, so the page never renders without it. Typed as
  // required so the firstSite.id dereferences below are sound.
  first_site: {
    id: string;
    name: string;
    domain: string;
  };
  gsc_expiring_connections?: GscExpiringConnection[];
  wp_degraded_connections?: { site_id: string; site_name: string; status: 'auth_failed' | 'unreachable' }[];
  service_health?: ServiceHealth[];
  /** Count of unacknowledged TrafficAlerts across the user's sites. */
  open_traffic_alerts_count?: number;
  /** Google algorithm updates that correlate with a traffic shift; [] hides the card. */
  algorithm_correlation?: AlgorithmCorrelation[];
  /**
   * Proven recovered-traffic payload for the RecoveryHero (DASH-ROI-HERO).
   *
   * ROI-ONE-NUMBER contract:
   *   `measured_clicks_gained` — gains-only canonical headline (positive significant
   *   deltas only, from getMeasuredBreakdown). This is the number every other ROI surface
   *   reports: RoiSummaryCard, the proof PDF, and the sparkline endpoint.
   *   Drive the RecoveryHero headline and the header gate from this field ONLY.
   *
   *   `total_clicks_lift` — net (gains minus losses, from getAggregateRoi). Shown ONLY
   *   as the muted "Net change (incl. losses)" secondary line when round(net) ≠ round(gains).
   *   Never used as the headline.
   *
   * `total_recommendations_tracked === 0` → tile hidden. `site_id` is first_site's
   * opaque public_id (HasPublicId) for the ROI link.
   *
   * `spark_28d` is the 28-day daily cumulative click-lift series for the sparkline.
   * Empty when < 2 real data points — component omits the sparkline. NEVER synthesize.
   */
  recovered_traffic: {
    /**
     * Gains-only canonical click lift (ROI-ONE-NUMBER). Headline source.
     * Sourced from getMeasuredBreakdown → sums only positive-delta significant snapshots.
     * Invariant: headline = sparkline endpoint = PDF click figure = RoiSummaryCard.
     */
    measured_clicks_gained: number;
    /**
     * Net (gains minus losses). Drives the muted "Net change (incl. losses)" secondary
     * line ONLY, shown iff Math.round(net) !== Math.round(measured_clicks_gained).
     * Never promoted to a headline.
     */
    total_clicks_lift: number;
    pages_improved: number;
    total_recommendations_tracked: number;
    site_id: string;
    /** Real 28-day daily cumulative-lift series. Empty = omit sparkline. */
    spark_28d: Array<{ day: number; cumulative_clicks_lift: number }>;
    /**
     * DASH-ROI-REVENUE: Estimated revenue recovered = measured_clicks_gained * user's $/click.
     * Null when the user has not set a $/click rate (or rate <= 0). Never fabricated.
     * Sourced from RoiProofOfValueService.buildMeasuredProofData() with the user's real rate.
     */
    revenue_estimate: number | null;
    /**
     * Human-readable label for the revenue estimate.
     * E.g. "at your $0.50/click — your estimate". Null when revenue_estimate is null.
     */
    revenue_estimate_label: string | null;
    /**
     * R22-DASH-ROI-CPC: The user's currently persisted $/click rate from UserSetting.
     * Null = never set. Passed to RecoveryHero to pre-fill the inline rate input.
     * Matches DashboardController::buildActivatedSiteData() roi_per_click_value.
     */
    roi_per_click_value: number | null;
  };
  content_intelligence: ContentIntelligence;
  activity?: UserActivity;
  streak?: { weeks: number; last_updated: string | null };
  streak_milestone_notification?: { id: string; weeks: number } | null;
  /**
   * DASH-ROI-RESULTS: compact summary for the Recovery Results strip.
   *
   * Sourced from RoiProofOfValueService::buildMeasuredProofData — the same
   * canonical values as MeasuredImpactCard and the ROI proof PDF.
   *
   * Null when: no first_site, FEATURE_ROI_PROOF_OF_VALUE=false, or gains === 0
   * (the controller omits the prop rather than sending {gains: 0}).
   * The strip gates on measured_gains_summary && measured_gains_summary.gains > 0,
   * but nulling at source is the safer contract.
   */
  measured_gains_summary?: {
    /** Recommendations with significant + positive delta. */
    gains: number;
    /** Sum of clicks_delta for gains-only recommendations. Always >= 0. */
    clicks_gained: number;
    /**
     * True when measurement window spans a seasonal period or correlates with a
     * core update. Must never be suppressed to make results look cleaner.
     */
    has_seasonal_caveat: boolean;
    /** first_site public_id (HasPublicId ULID) for the ROI deep-link. */
    site_id: string;
  } | null;
  /** CAMPAIGN-008: user's referral URL (null when referral feature not yet enabled) */
  referral_url?: string | null;
  /**
   * COMP-007: top query position movements (day-over-day deltas from GscQueryMetric).
   * Empty array = strip hidden. Backend wires this once GscQueryMetric diff is ready.
   */
  position_delta_queries?: PositionDeltaQuery[];
  /**
   * R8UX-012 / PMR-004: Count of AI drafts in the UNTRIAGED (pending review) state
   * for the first_site. When > 0, the ReviewQueueHero is shown as the primary hero
   * (resume-first: finish what's already queued before starting a new run).
   * 0 = no drafts pending review (hero hidden).
   */
  pending_review_count?: number;
  /**
   * R16-PD-02: The most recent pending/processing BatchJob for first_site, or null
   * when no batch is active. When present, RecoveryRunHero shows a "Resume run"
   * primary CTA instead of "Start Recovery Run".
   */
  in_flight_batch?: InFlightBatch | null;
}

/** R16-PD-02: Serialized shape of the active BatchJob for the resume affordance. */
export interface InFlightBatch {
  /** BatchJob.public_id (opaque ULID). */
  id: string;
  status: string;
  total_jobs: number;
  completed_jobs: number;
  /** Awaiting-review drafts scoped to this batch (for "N drafts awaiting review" copy). */
  pending_review_count: number;
}
