// #region ValidationRunDetailModel [C:3] [TYPE Model] [SEMANTICS validation,run,detail,dashboard,collapsible,model]
// @ingroup Models
// @BRIEF Screen model for validation run detail — dashboard-by-dashboard results with screenshots, issues, and task logs.
// @INVARIANT Screenshot blob URLs are cached per (dashboardId, tabIndex) key. Revoked when cache key is overwritten.
// @ACTION toggleDashboard(id) / toggleLogsSent(key) / toggleTaskLogs(key) — Collapsible section toggles.
// @ACTION loadScreenshot(path, key) — Loads screenshot blob from storage API.
// @RELATION DEPENDS_ON -> [ApiModule]

import { api } from '$lib/api.js';
import type { TaskLogEntry } from '$lib/api.js';
import { SvelteSet } from "svelte/reactivity";

type UxState = 'idle' | 'loading' | 'populated' | 'error' | 'not_found';

interface RunData {
  id?: string;
  task_id?: string;
  policy_id?: string;
  status?: string;
  started_at?: string;
  finished_at?: string;
  trigger_type?: string;
  dashboards?: DashboardResult[];
  results?: DashboardResult[];
}

interface DashboardResult {
  id?: string;
  dashboard_id?: string;
  title?: string;
  slug?: string;
  status?: string;
  duration_ms?: number;
  path?: string;
  tabs?: TabResult[];
  screenshots?: ScreenshotResult[];
  issues?: IssueResult[];
  datasets?: unknown[];
  charts?: unknown[];
  logs_sent_to_llm?: string[];
  dataset_health?: unknown;
  _taskLogsLoaded?: boolean;
  task_logs?: TaskLogEntry[];
}

interface TabResult {
  label?: string;
  index?: number;
  [key: string]: unknown;
}

interface ScreenshotResult {
  path?: string;
  [key: string]: unknown;
}

interface IssueResult {
  severity?: string;
  message?: string;
  location?: string;
  tab_index?: number | string;
}

export class ValidationRunDetailModel {
  // ── Atoms ─────────────────────────────────────────────────────
  uxState: UxState = $state('populated');
  run: RunData | null = $state(null);
  errorMsg: string | null = $state(null);
  expandedDashboards: Set<string> = $state(new SvelteSet());
  selectedScreenshotTab: Record<string, string> = $state({});
  screenshotUrls: Record<string, string> = $state({});
  logsSentExpanded: Set<string> = $state(new SvelteSet());
  taskLogsExpanded: Set<string> = $state(new SvelteSet());

  // ── Derived ───────────────────────────────────────────────────
  dashboards = $derived(this.run?.dashboards || this.run?.results || []);
  passCount = $derived(this.dashboards.filter((d: DashboardResult) => d.status === 'PASS').length);
  warnCount = $derived(this.dashboards.filter((d: DashboardResult) => d.status === 'WARN').length);
  failCount = $derived(this.dashboards.filter((d: DashboardResult) => d.status === 'FAIL').length);
  unknownCount = $derived(this.dashboards.filter((d: DashboardResult) => !d.status || d.status === 'UNKNOWN').length);
  totalIssues = $derived(this.dashboards.reduce((s: number, d: DashboardResult) => s + (d.issues?.length || 0), 0));

  // ── Actions ───────────────────────────────────────────────────

  toggleDashboard(dashboardId: string): void {
    const next = new SvelteSet(this.expandedDashboards);
    if (next.has(dashboardId)) {
      next.delete(dashboardId);
    } else {
      next.add(dashboardId);
      if (!this.selectedScreenshotTab[dashboardId]) {
        this.selectedScreenshotTab = { ...this.selectedScreenshotTab, [dashboardId]: '0' };
        const db = this.dashboards.find((d: DashboardResult) => d.id === dashboardId || d.dashboard_id === dashboardId);
        if (db?.screenshots?.length) {
          const tabKey = `${dashboardId}_0`;
          if (!this.screenshotUrls[tabKey]) {
            const path = db.screenshots[0]?.path || String(db.screenshots[0] || '');
            this.loadScreenshot(path, tabKey);
          }
        }
      }
    }
    this.expandedDashboards = next;
  }

  isDashboardExpanded(dashboardId: string): boolean {
    return this.expandedDashboards.has(dashboardId);
  }

  toggleLogsSent(key: string): void {
    const next = new SvelteSet(this.logsSentExpanded);
    next.has(key) ? next.delete(key) : next.add(key);
    this.logsSentExpanded = next;
  }

  toggleTaskLogs(key: string): void {
    const next = new SvelteSet(this.taskLogsExpanded);
    if (next.has(key)) {
      next.delete(key);
    } else {
      next.add(key);
      const dId = key.replace('tasklogs_', '');
      const db = this.dashboards.find((d: DashboardResult) => d.id === dId || d.dashboard_id === dId) as DashboardResult;
      if (db && !db._taskLogsLoaded && this.run?.task_id) {
        db._taskLogsLoaded = true;
        api.getTaskLogs(this.run.task_id as string, { limit: 200 }).then((logs: unknown) => {
          db.task_logs = (logs as TaskLogEntry[]) || [];
        }).catch(() => { db.task_logs = []; });
      }
    }
    this.taskLogsExpanded = next;
  }

  async loadScreenshot(screenshotPath: string, cacheKey: string): Promise<void> {
    if (this.screenshotUrls[cacheKey]) return;
    try {
      const blob = await api.getStorageFileBlob(screenshotPath);
      this.screenshotUrls = { ...this.screenshotUrls, [cacheKey]: URL.createObjectURL(blob) };
    } catch { /* silently fail */ }
  }

  openScreenshot(url: string): void {
    window.open(url, '_blank');
  }

  // ── Static utilities ──────────────────────────────────────────

  static getStatusDotClass(s: string | undefined): string {
    const map: Record<string, string> = { PASS: 'text-success', WARN: 'text-warning', FAIL: 'text-destructive', UNKNOWN: 'text-text-subtle' };
    return map[s ?? ''] || 'text-text-subtle';
  }

  static getStatusDot(s: string | undefined): string {
    const dots: Record<string, string> = { PASS: '🟢', WARN: '🟡', FAIL: '🔴', UNKNOWN: '⚪' };
    return dots[s ?? ''] || dots.UNKNOWN;
  }

  static getSeverityClass(severity: string | undefined): string {
    const map: Record<string, string> = { critical: 'bg-destructive-light text-destructive', high: 'bg-warning-light text-warning', medium: 'bg-warning-light text-warning', low: 'bg-primary-light text-primary', info: 'bg-surface-muted text-text-muted', error: 'bg-destructive-light text-destructive', warning: 'bg-warning-light text-warning' };
    return map[severity?.toLowerCase() ?? ''] || 'bg-surface-muted text-text-muted';
  }

  static formatDuration(ms: number | undefined): string {
    if (!ms && ms !== 0) return '-';
    if (ms < 1000) return `${ms}ms`;
    if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
    return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
  }

  static getTriggerLabel(trigger: string | undefined, tFn: (_key: string) => string): string {
    if (trigger === 'manual') return tFn('validation.trigger_manual') || 'Manual';
    if (trigger === 'scheduled') return tFn('validation.trigger_scheduled') || 'Scheduled';
    return trigger || '-';
  }

  static getPathLabel(path: string | undefined, tFn: (_key: string) => string): string {
    if (path === 'A') return tFn('validation.path_a') || 'Path A';
    if (path === 'B') return tFn('validation.path_b') || 'Path B';
    return path || '-';
  }

  static getPathBadgeClass(path: string | undefined): string {
    if (path === 'A') return 'bg-info-light text-info';
    if (path === 'B') return 'bg-success-light text-success';
    return 'bg-surface-muted text-text-muted';
  }

  static getTabIssueSeverity(tabLabel: string, tabIdx: number, totalTabs: number, issues: IssueResult[]): { count: number; severity: string } | null {
    if (!issues?.length) return null;
    const byIndex = issues.filter(i => i.tab_index === tabIdx || i.tab_index === String(tabIdx));
    if (byIndex.length > 0) {
      const worst = byIndex.some(i => i.severity === 'FAIL') ? 'FAIL' : byIndex.some(i => i.severity === 'WARN') ? 'WARN' : 'INFO';
      return { count: byIndex.length, severity: worst };
    }
    const lowerLabel = tabLabel?.toLowerCase();
    let matching: IssueResult[] = [];
    if (lowerLabel) matching = issues.filter(i => i.location?.toLowerCase().includes(lowerLabel) || lowerLabel.includes(i.location?.toLowerCase() || ''));
    const unmatched = issues.filter(i => i.tab_index === -1 || i.tab_index === '-1' || i.tab_index === undefined || i.tab_index === null);
    if (matching.length === 0 && tabIdx === totalTabs - 1 && unmatched.length > 0) {
      const worst = unmatched.some(i => i.severity === 'FAIL') ? 'FAIL' : unmatched.some(i => i.severity === 'WARN') ? 'WARN' : 'INFO';
      return { count: unmatched.length, severity: worst };
    }
    if (matching.length === 0) return null;
    const worst = matching.some(i => i.severity === 'FAIL') ? 'FAIL' : matching.some(i => i.severity === 'WARN') ? 'WARN' : 'INFO';
    return { count: matching.length, severity: worst };
  }
}
// #endregion ValidationRunDetailModel
