// frontend/src/lib/models/GitStatusModel.svelte.ts
// #region GitStatusModel [C:4] [TYPE Model] [SEMANTICS git,status,dashboard,sync,screen-model]
// @BRIEF State model for a dashboard's git repository status and sync operations.
// @INVARIANT Sync/pull/push operations are mutually exclusive — only one runs at a time.
// @ACTION syncRepository() — sync dashboard state to Git (returns true on success)
// @ACTION pullRepository() — pull latest changes from remote
// @ACTION pushRepository() — push local commits to remote
// @ACTION loadStatus() — fetch current git status
// @ACTION loadDiffPreview() — fetch staged + unstaged diff
// @ACTION loadHistory() — fetch commit history
// @RELATION DEPENDS_ON -> [EXT:frontend:gitService]

import { gitService } from '../../services/gitService.js';
import { addToast } from '$lib/toasts.svelte.js';
import { getT } from '$lib/i18n/index.svelte.js';

// ── Types ─────────────────────────────────────────────────────

interface GitStatusPayload {
  sync_state?: string;
  current_branch?: string | null;
  has_repo?: boolean;
  modified_files?: string[];
  staged_files?: string[];
  untracked_files?: string[];
  ahead_count?: number;
  behind_count?: number;
  [key: string]: unknown;
}

interface MetaInfo {
  dotClass: string;
  pillClass: string;
  label: string;
}

interface CommitEntry {
  id: string;
  message: string;
  [key: string]: unknown;
}

/** short alias for getT() */
function tt(): Record<string, unknown> {
  return getT();
}

export class GitStatusModel {
  // ── Identity ───────────────────────────────────────────────
  /** Dashboard slug or reference used for git API calls. */
  dashboardId: string = $state('');
  /** Environment ID for scoped git operations. */
  envId: string | null = $state(null);

  // ── Git Status ─────────────────────────────────────────────
  /** Raw git status payload from the API. */
  gitStatus: GitStatusPayload | null = $state(null);
  /** True while fetching git status. */
  statusLoading: boolean = $state(false);
  /** Error message from last status fetch, or null. */
  statusError: string | null = $state(null);

  // ── Git Diff Preview ───────────────────────────────────────
  /** Concatenated staged + unstaged diff string. */
  diffPreview: string = $state('');
  /** True while loading diff preview. */
  diffLoading: boolean = $state(false);

  // ── Sync Operations ────────────────────────────────────────
  /** True while a git sync operation is in progress. */
  syncing: boolean = $state(false);
  /** True while a git pull is in progress. */
  pulling: boolean = $state(false);
  /** True while a git push is in progress. */
  pushing: boolean = $state(false);

  // ── Branch ─────────────────────────────────────────────────
  /** Current active branch name. */
  currentBranch: string = $state('main');

  // ── Commit History ─────────────────────────────────────────
  /** Array of commit objects from getHistory(). */
  gitHistory: CommitEntry[] = $state([]);
  /** True while loading commit history. */
  historyLoading: boolean = $state(false);

  // ── Derived State ──────────────────────────────────────────

  /** True when the dashboard has an initialized git repository. */
  hasGitRepo: boolean = $derived.by(() => {
    if (!this.gitStatus) return false;
    if (this.gitStatus.has_repo === false) return false;
    return this.gitStatus.sync_state !== 'NO_REPO' || Boolean(this.gitStatus.current_branch);
  });

  /**
   * Resolved sync state label.
   * One of: NO_REPO, SYNCED, CHANGES, AHEAD_REMOTE, BEHIND_REMOTE, DIVERGED.
   */
  syncState: string = $derived.by(() => {
    if (!this.gitStatus || this.gitStatus.has_repo === false) return 'NO_REPO';
    const explicitState = String(this.gitStatus.sync_state || '').toUpperCase();
    const changedCount =
      (this.gitStatus?.modified_files?.length || 0) +
      (this.gitStatus?.staged_files?.length || 0) +
      (this.gitStatus?.untracked_files?.length || 0);
    const ahead = Number(this.gitStatus?.ahead_count || 0);
    const behind = Number(this.gitStatus?.behind_count || 0);
    if (explicitState && explicitState !== 'UNKNOWN' && explicitState !== 'NO_REPO') {
      return explicitState;
    }
    if (changedCount > 0) return 'CHANGES';
    if (ahead > 0 && behind > 0) return 'DIVERGED';
    if (ahead > 0) return 'AHEAD_REMOTE';
    if (behind > 0) return 'BEHIND_REMOTE';
    if (this.gitStatus?.current_branch) return 'SYNCED';
    return 'NO_REPO';
  });

  /** All changed files (staged + modified + untracked), deduplicated. */
  allChangedFilesList: string[] = $derived.by(() => {
    if (!this.gitStatus) return [];
    const staged = this.gitStatus.staged_files || [];
    const modified = this.gitStatus.modified_files || [];
    const untracked = this.gitStatus.untracked_files || [];
    return [...staged, ...modified, ...untracked].filter(
      (file: string, idx: number, arr: string[]) => arr.indexOf(file) === idx,
    );
  });

  /** Number of changed files matching chart paths (/charts/ or charts/). */
  changedChartsCount: number = $derived.by(() => {
    return this.allChangedFilesList.filter(
      (f: string) => f.includes('/charts/') || f.includes('charts/'),
    ).length;
  });

  /** Number of changed files matching dataset paths (/datasets/ or datasets/). */
  changedDatasetsCount: number = $derived.by(() => {
    return this.allChangedFilesList.filter(
      (f: string) => f.includes('/datasets/') || f.includes('datasets/'),
    ).length;
  });

  /** True when there are any changes to commit. */
  hasChangesToCommit: boolean = $derived.by(() => this.allChangedFilesList.length > 0);

  /** Display metadata (dotClass, pillClass, label) for the current sync state. */
  meta: MetaInfo = $derived.by(() => {
    const syncState = this.syncState;
    const _t = tt();
    if (syncState === 'SYNCED') {
      return {
        dotClass: 'bg-emerald-500',
        pillClass: 'bg-emerald-100 text-emerald-800 border-emerald-200',
        label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.synced || 'Synced',
      };
    }
    if (syncState === 'CHANGES') {
      return {
        dotClass: 'bg-amber-500',
        pillClass: 'bg-amber-100 text-amber-800 border-amber-200',
        label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.changes || 'Modified',
      };
    }
    if (syncState === 'AHEAD_REMOTE') {
      return {
        dotClass: 'bg-blue-500',
        pillClass: 'bg-blue-100 text-blue-800 border-blue-200',
        label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.ahead_remote || 'Ahead',
      };
    }
    if (syncState === 'BEHIND_REMOTE') {
      return {
        dotClass: 'bg-indigo-500',
        pillClass: 'bg-indigo-100 text-indigo-800 border-indigo-200',
        label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.behind_remote || 'Behind',
      };
    }
    if (syncState === 'DIVERGED') {
      return {
        dotClass: 'bg-rose-500',
        pillClass: 'bg-rose-100 text-rose-800 border-rose-200',
        label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.diverged || 'Diverged',
      };
    }
    return {
      dotClass: 'bg-slate-400',
      pillClass: 'bg-slate-100 text-slate-700 border-slate-200',
      label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.no_repo || 'Uninitialized',
    };
  });

  constructor() {}

  // ── Initialization ─────────────────────────────────────────

  /**
   * Set dashboard reference and envId for git operations.
   */
  initialize(dashboardId: string, envId: string | null = null): void {
    this.dashboardId = dashboardId;
    this.envId = envId;
  }

  // ── Status ─────────────────────────────────────────────────

  /**
   * Fetch current git status for the dashboard.
   * @SIDE_EFFECT Sets statusLoading, gitStatus, statusError, currentBranch.
   */
  async loadStatus(): Promise<void> {
    if (!this.dashboardId) return;
    this.statusLoading = true;
    this.statusError = null;
    this.diffPreview = '';
    try {
      const status: GitStatusPayload = await gitService.getStatus(this.dashboardId, this.envId || null);
      this.gitStatus = status;
      if (status?.current_branch) {
        this.currentBranch = status.current_branch;
      }
    } catch (err: unknown) {
      this.statusError = err instanceof Error ? err.message : 'Failed to load Git status';
      this.gitStatus = {
        sync_state: 'NO_REPO',
        current_branch: null,
        has_repo: false,
        modified_files: [],
        staged_files: [],
        untracked_files: [],
        ahead_count: 0,
        behind_count: 0,
      };
    } finally {
      this.statusLoading = false;
    }
  }

  // ── Diff Preview ───────────────────────────────────────────

  /**
   * Load staged and unstaged diff preview.
   * @SIDE_EFFECT Sets diffPreview, diffLoading; shows toast when no changes.
   */
  async loadDiffPreview(): Promise<void> {
    if (!this.hasGitRepo) return;
    this.diffLoading = true;
    try {
      const staged: string = await gitService.getDiff(this.dashboardId, null, true, this.envId || null);
      const unstaged: string = await gitService.getDiff(this.dashboardId, null, false, this.envId || null);
      this.diffPreview = [staged, unstaged].filter(Boolean).join('\n\n');
      if (!this.diffPreview) {
        addToast((tt().git as Record<string, string>)?.no_changes || 'No changes detected', 'info');
      }
    } catch (err: unknown) {
      addToast(err instanceof Error ? err.message : 'Failed to load diff', 'error');
    } finally {
      this.diffLoading = false;
    }
  }

  // ── Sync ───────────────────────────────────────────────────

  /**
   * Synchronize dashboard state with Git repository.
   * @SIDE_EFFECT POSTs to /sync; shows toast; reloads status.
   * @returns {Promise<boolean>} true on success, false on failure or guard skip.
   */
  async syncRepository(): Promise<boolean> {
    if (!this.hasGitRepo) {
      addToast((tt().git as Record<string, string>)?.not_linked || 'Repository is not initialized', 'error');
      return false;
    }
    if (this.syncing) return false;
    this.syncing = true;
    try {
      await gitService.sync(this.dashboardId, this.envId || null, this.envId || null);
      addToast((tt().git as Record<string, string>)?.sync_success || 'Dashboard state synced to Git', 'success');
      await this.loadStatus();
      return true;
    } catch (err: unknown) {
      addToast(err instanceof Error ? err.message : 'Git sync failed', 'error');
      return false;
    } finally {
      this.syncing = false;
    }
  }

  // ── Pull ───────────────────────────────────────────────────

  /**
   * Pull latest changes from remote repository.
   * @SIDE_EFFECT POSTs to /pull; shows toast; reloads status.
   */
  async pullRepository(): Promise<void> {
    if (!this.hasGitRepo || this.pulling) return;
    this.pulling = true;
    try {
      await gitService.pull(this.dashboardId, this.envId || null);
      addToast((tt().git as Record<string, string>)?.pull_success || 'Changes pulled from remote', 'success');
      await this.loadStatus();
    } catch (err: unknown) {
      addToast(err instanceof Error ? err.message : 'Git pull failed', 'error');
    } finally {
      this.pulling = false;
    }
  }

  // ── Push ───────────────────────────────────────────────────

  /**
   * Push local commits to remote repository.
   * @SIDE_EFFECT POSTs to /push; shows toast; reloads status.
   */
  async pushRepository(): Promise<void> {
    if (!this.hasGitRepo || this.pushing) return;
    this.pushing = true;
    try {
      await gitService.push(this.dashboardId, this.envId || null);
      addToast((tt().git as Record<string, string>)?.push_success || 'Changes pushed to remote', 'success');
      await this.loadStatus();
    } catch (err: unknown) {
      addToast(err instanceof Error ? err.message : 'Git push failed', 'error');
    } finally {
      this.pushing = false;
    }
  }

  // ── History ────────────────────────────────────────────────

  /**
   * Fetch commit history.
   * @SIDE_EFFECT Sets gitHistory, historyLoading.
   */
  async loadHistory(limit: number = 50): Promise<void> {
    if (!this.dashboardId) return;
    this.historyLoading = true;
    try {
      this.gitHistory = await gitService.getHistory(this.dashboardId, limit, this.envId || null);
    } catch (err: unknown) {
      this.gitHistory = [];
      addToast(err instanceof Error ? err.message : 'Failed to load history', 'error');
    } finally {
      this.historyLoading = false;
    }
  }

  // ── Branch ─────────────────────────────────────────────────

  /**
   * Handle branch change from a BranchSelector event.
   */
  handleBranchChange(event: { detail?: { branch?: string } }): void {
    const nextBranch = event?.detail?.branch;
    if (!nextBranch) return;
    this.currentBranch = nextBranch;
    void this.loadStatus();
  }
}
// #endregion GitStatusModel
