/**
 * ProfileManager - Persistent OpenChrome Profile Architecture
 *
 * Manages a persistent Chrome profile directory at ~/.openchrome/profile/
 * instead of creating disposable temp profiles on every launch.
 * Provides atomic cookie sync using the SQLite backup API.
 */

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execFileSync } from 'child_process';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type ProfileType = 'real' | 'persistent' | 'temp' | 'explicit' | 'headless-shell';

export interface SyncMetadata {
  lastSyncTimestamp: number;
  /** `${mtimeMs}:${size}` of the source Cookies file at sync time */
  sourceProfileHash: string;
  syncCount: number;
  sourceProfileDir: string;
}

export interface ProfileResolution {
  userDataDir: string;
  profileType: ProfileType;
  syncPerformed: boolean;
  /** The resolved profile directory name (e.g., "Default", "Profile 1") */
  profileDirectory?: string;
}

export interface ChromeProfileInfo {
  /** Internal directory name (e.g., "Default", "Profile 1", "Profile 2") */
  directory: string;
  /** User-visible display name (e.g., "Personal", "Work") */
  name: string;
  /** User name / email associated with the profile (if available) */
  userName?: string;
  /** Whether this is the active/last-used profile */
  isActive?: boolean;
}

// ---------------------------------------------------------------------------
// ProfileManager
// ---------------------------------------------------------------------------

/**
 * Manages persistent OpenChrome Chrome profiles and cookie synchronisation.
 *
 * Key responsibilities:
 * - Maintain a reusable profile at `~/.openchrome/profile/` so cookies and
 *   session data survive across OpenChrome restarts.
 * - Track whether the persistent profile is stale relative to the real
 *   Chrome profile using lightweight file-stat hashing.
 * - Perform atomic cookie sync via the `sqlite3` CLI `.backup` command when
 *   available.
 * - Decide which profile directory Chrome should use (`resolveProfile`).
 */
export class ProfileManager {
  // -------------------------------------------------------------------------
  // Constants (configurable via Object.defineProperty for testing)
  // -------------------------------------------------------------------------

  /** Root directory for the persistent OpenChrome profile. */
  static readonly PERSISTENT_PROFILE_DIR = path.join(
    os.homedir(),
    '.openchrome',
    'profile'
  );

  /** Path to the JSON file that tracks the last sync state. */
  static readonly SYNC_METADATA_PATH = path.join(
    os.homedir(),
    '.openchrome',
    'sync-metadata.json'
  );

  /** Cookie data is considered fresh if synced within this window (30 min). */
  static readonly COOKIE_FRESHNESS_MS = 30 * 60 * 1000;

  // -------------------------------------------------------------------------
  // Public methods
  // -------------------------------------------------------------------------

  /**
   * Get the default Chrome user data directory for the current platform.
   * Returns null if Chrome data directory is not found.
   */
  getDefaultUserDataDir(): string | null {
    const platform = os.platform();
    const home = os.homedir();

    if (platform === 'darwin') {
      const dir = path.join(home, 'Library', 'Application Support', 'Google', 'Chrome');
      if (fs.existsSync(dir)) return dir;
    } else if (platform === 'win32') {
      const localAppData = process.env['LOCALAPPDATA'] || path.join(home, 'AppData', 'Local');
      const dir = path.join(localAppData, 'Google', 'Chrome', 'User Data');
      if (fs.existsSync(dir)) return dir;
    } else {
      const candidates = [
        path.join(home, '.config', 'google-chrome'),
        path.join(home, '.config', 'chromium'),
        path.join(home, 'snap', 'chromium', 'current', '.config', 'chromium'),
      ];
      for (const dir of candidates) {
        if (fs.existsSync(dir)) return dir;
      }
    }
    return null;
  }

  /**
   * List available Chrome profiles by reading the Local State file.
   * Returns profile info sorted by directory name.
   */
  listProfiles(userDataDir?: string): ChromeProfileInfo[] {
    const dir = userDataDir || this.getDefaultUserDataDir();
    if (!dir) return [];

    const localStatePath = path.join(dir, 'Local State');
    try {
      const raw = fs.readFileSync(localStatePath, 'utf8');
      const localState = JSON.parse(raw);
      const infoCache = localState?.profile?.info_cache;
      if (!infoCache || typeof infoCache !== 'object') return [];

      const lastUsed = localState?.profile?.last_used;

      return Object.entries(infoCache)
        .map(([directory, info]: [string, any]) => ({
          directory,
          name: info?.name || directory,
          ...(info?.user_name && { userName: info.user_name }),
          ...(lastUsed === directory && { isActive: true }),
        }))
        .sort((a, b) => a.directory.localeCompare(b.directory));
    } catch {
      return [];
    }
  }

  /**
   * Return the persistent profile directory, creating it (including the
   * `Default/` subdirectory) if it does not already exist.
   */
  getOrCreatePersistentProfile(): string {
    const profileDir = ProfileManager.PERSISTENT_PROFILE_DIR;
    const defaultDir = path.join(profileDir, 'Default');

    if (!fs.existsSync(profileDir)) {
      fs.mkdirSync(profileDir, { recursive: true });
      console.error(
        `[ProfileManager] Created persistent profile directory: ${profileDir}`
      );
    }

    if (!fs.existsSync(defaultDir)) {
      fs.mkdirSync(defaultDir, { recursive: true });
    }

    return profileDir;
  }

  /**
   * Determine whether the persistent profile needs a fresh cookie sync from
   * `sourceDir`.
   *
   * Returns `true` when:
   * - No sync metadata exists (never synced before), OR
   * - The source Cookies file has changed since the last sync (different
   *   mtime or size), OR
   * - The last sync is older than `COOKIE_FRESHNESS_MS`.
   */
  needsSync(sourceDir: string, profileSubdir: string = 'Default'): boolean {
    const metadata = this.getSyncMetadata();

    if (!metadata) {
      return true; // Never synced
    }

    // Compute current hash of the source Cookies file
    const sourceCookiesPath = path.join(sourceDir, profileSubdir, 'Cookies');
    let currentHash: string;
    try {
      const stat = fs.statSync(sourceCookiesPath);
      currentHash = `${stat.mtimeMs}:${stat.size}`;
    } catch {
      // Cookies file doesn't exist in source — no sync possible
      if (!metadata) {
        console.error('[ProfileManager] Source Cookies not found and no prior sync — persistent profile will have no cookies');
      }
      return false;
    }

    // Guard: if the persistent profile's Cookies file has been modified after
    // the last sync, a headless session wrote cookies — do not overwrite them.
    const persistentCookiesPath = path.join(
      ProfileManager.PERSISTENT_PROFILE_DIR,
      profileSubdir,
      'Cookies'
    );
    try {
      const persistentStat = fs.statSync(persistentCookiesPath);
      if (persistentStat.mtimeMs > metadata.lastSyncTimestamp) {
        console.error(
          '[ProfileManager] Persistent profile cookies modified after last sync — skipping overwrite to preserve headless-acquired session'
        );
        return false;
      }
    } catch {
      // Persistent Cookies file doesn't exist yet — sync is needed
    }

    if (currentHash !== metadata.sourceProfileHash) {
      return true; // Source has changed
    }

    if (Date.now() - metadata.lastSyncTimestamp > ProfileManager.COOKIE_FRESHNESS_MS) {
      return true; // Stale
    }

    return false;
  }

  /**
   * Synchronise cookies, localStorage, IndexedDB, and Preferences from
   * `sourceDir` into `destDir`.
   *
   * Uses the `sqlite3` CLI `.backup` command for an atomic, consistent
   * snapshot of the Cookies database. When `sqlite3` is not available,
   * cookie sync is skipped because copying Cookies/WAL/SHM files sequentially
   * can produce an inconsistent snapshot. Non-cookie profile data is still
   * copied.
   *
   * After a successful sync the metadata file is updated via
   * `updateSyncMetadata`.
   *
   * @returns `{ atomic: true, success: true }` when sqlite3 backup was used,
   *          `{ atomic: false, success: false }` when cookie sync was skipped,
   *          `{ atomic: false, success: false }` when all methods failed.
   */
  syncProfileData(
    sourceDir: string,
    destDir: string,
    profileSubdir: string = 'Default'
  ): { atomic: boolean; success: boolean } {
    try {
      const destDefault = path.join(destDir, profileSubdir);
      fs.mkdirSync(destDefault, { recursive: true });

      // --- 1. Copy Local State -----------------------------------------------
      const localStateSrc = path.join(sourceDir, 'Local State');
      if (fs.existsSync(localStateSrc)) {
        fs.copyFileSync(localStateSrc, path.join(destDir, 'Local State'));
      }

      // --- 2. Sync Cookies (atomic via sqlite3, or plain copy fallback) ------
      const sourceCookiesPath = path.join(sourceDir, profileSubdir, 'Cookies');
      const destCookiesPath = path.join(destDefault, 'Cookies');
      let atomic = false;

      if (fs.existsSync(sourceCookiesPath)) {
        const sqlite3Available = this._isSqlite3Available();

        if (sqlite3Available) {
          // Atomic backup using the SQLite .backup command.
          // This works even when Chrome is actively writing to the DB.
          // Uses execFileSync (no shell) to prevent injection via path characters.
          if (process.platform === 'win32' && destCookiesPath.includes('"')) {
            throw new Error('sqlite3 .backup: destination path contains \'"\', cannot quote safely on Windows');
          }
          const backupCmd = process.platform === 'win32'
            ? `.backup "${destCookiesPath}"`
            : `.backup '${destCookiesPath.replace(/'/g, "''")}'`;
          execFileSync('sqlite3', [
            sourceCookiesPath,
            backupCmd,
          ], { stdio: 'ignore', timeout: 10000 });

          // .backup produces a clean WAL-checkpoint DB — remove stale WAL/SHM/journal
          // at the destination so Chrome doesn't get confused.
          for (const suffix of ['Cookies-wal', 'Cookies-shm', 'Cookies-journal']) {
            const stale = path.join(destDefault, suffix);
            if (fs.existsSync(stale)) {
              try {
                fs.unlinkSync(stale);
              } catch {
                // Non-fatal
              }
            }
          }

          atomic = true;
        } else {
          console.error(
            '[ProfileManager] sqlite3 not found, skipping cookie sync to avoid non-atomic WAL snapshot'
          );
        }
      }

      // --- 2b. Copy Local Storage (LevelDB) ----------------------------------
      const localStorageSrc = path.join(sourceDir, profileSubdir, 'Local Storage');
      const localStorageDest = path.join(destDefault, 'Local Storage');
      if (fs.existsSync(localStorageSrc)) {
        try {
          this._copyDirectoryRecursive(localStorageSrc, localStorageDest);
        } catch (err) {
          console.error('[ProfileManager] Local Storage copy failed (non-fatal):', err);
        }
      }

      // --- 2c. Copy IndexedDB -------------------------------------------------
      const indexedDBSrc = path.join(sourceDir, profileSubdir, 'IndexedDB');
      const indexedDBDest = path.join(destDefault, 'IndexedDB');
      if (fs.existsSync(indexedDBSrc)) {
        try {
          this._copyDirectoryRecursive(indexedDBSrc, indexedDBDest);
        } catch (err) {
          console.error('[ProfileManager] IndexedDB copy failed (non-fatal):', err);
        }
      }

      // --- 3. Copy and patch Preferences ------------------------------------
      const prefsSrc = path.join(sourceDir, profileSubdir, 'Preferences');
      if (fs.existsSync(prefsSrc)) {
        try {
          const prefsContent = fs.readFileSync(prefsSrc, 'utf8');
          const prefs = JSON.parse(prefsContent);

          // Prevent "Chrome didn't shut down correctly" prompt
          if (prefs.profile) {
            prefs.profile.exit_type = 'Normal';
            prefs.profile.exited_cleanly = true;
          }

          // Suppress session restore so copied profile doesn't reopen old tabs
          if (!prefs.session) prefs.session = {};
          prefs.session.restore_on_startup = 5; // 5 = open new tab page
          delete prefs.session.startup_urls;

          fs.writeFileSync(
            path.join(destDefault, 'Preferences'),
            JSON.stringify(prefs)
          );
        } catch {
          // JSON parse failed — skip Preferences entirely.
          // Chrome will create fresh defaults.
        }
      }

      // --- 4. Update cookie sync metadata -----------------------------------
      if (atomic) {
        this.updateSyncMetadata(sourceDir, profileSubdir);
      }

      console.error(
        `[ProfileManager] Profile data sync complete (atomic=${atomic}) from ${sourceDir} → ${destDir}`
      );
      return { atomic, success: atomic };
    } catch (err) {
      console.error(
        '[ProfileManager] syncProfileData failed (non-fatal):',
        err
      );
      return { atomic: false, success: false };
    }
  }

  /**
   * Read the current sync metadata from disk.
   *
   * @returns Parsed `SyncMetadata` or `null` if the file does not exist or
   *          cannot be parsed.
   */
  getSyncMetadata(): SyncMetadata | null {
    try {
      const raw = fs.readFileSync(ProfileManager.SYNC_METADATA_PATH, 'utf8');
      return JSON.parse(raw) as SyncMetadata;
    } catch {
      return null;
    }
  }

  /**
   * Persist sync metadata for `sourceDir` using a temp-file + rename pattern
   * to prevent corruption on concurrent writes.
   */
  updateSyncMetadata(sourceDir: string, profileSubdir: string = 'Default'): void {
    try {
      // Compute source hash
      const sourceCookiesPath = path.join(sourceDir, profileSubdir, 'Cookies');
      let sourceProfileHash = '';
      try {
        const stat = fs.statSync(sourceCookiesPath);
        sourceProfileHash = `${stat.mtimeMs}:${stat.size}`;
      } catch {
        // Cookies file missing — leave hash empty
      }

      const existing = this.getSyncMetadata();
      const updated: SyncMetadata = {
        lastSyncTimestamp: Date.now(),
        sourceProfileHash,
        syncCount: existing ? existing.syncCount + 1 : 1,
        sourceProfileDir: sourceDir,
      };

      const metaPath = ProfileManager.SYNC_METADATA_PATH;
      const metaDir = path.dirname(metaPath);

      // Ensure parent directory exists
      if (!fs.existsSync(metaDir)) {
        fs.mkdirSync(metaDir, { recursive: true });
      }

      // Atomic write: write to temp file, then rename
      const tmpPath = `${metaPath}.tmp-${Date.now()}`;
      fs.writeFileSync(tmpPath, JSON.stringify(updated, null, 2), 'utf8');
      fs.renameSync(tmpPath, metaPath);
    } catch (err) {
      console.error('[ProfileManager] updateSyncMetadata failed (non-fatal):', err);
    }
  }

  /**
   * Determine which Chrome `userDataDir` to use and whether a cookie sync
   * was performed.
   *
   * Priority order:
   * 1. `explicitUserDataDir` — caller has specified an exact directory.
   * 2. `useTempProfile` or `usingHeadlessShell` — create a fresh temp dir.
   * 3. `realProfileDir` exists, **not** locked, and `isAutoLaunch` is false —
   *    use real profile directly.
   * 4. `realProfileDir` exists and is **locked**, OR `isAutoLaunch` is true —
   *    use persistent profile, syncing cookies from the real profile when stale.
   * 5. No `realProfileDir` — use persistent profile without a sync.
   */
  resolveProfile(options: {
    realProfileDir: string | null;
    isProfileLocked: boolean;
    explicitUserDataDir?: string;
    useTempProfile?: boolean;
    usingHeadlessShell?: boolean;
    profileDirectory?: string;
    isAutoLaunch?: boolean;
  }): ProfileResolution {
    const {
      realProfileDir,
      isProfileLocked,
      explicitUserDataDir,
      useTempProfile,
      usingHeadlessShell,
      profileDirectory,
      isAutoLaunch,
    } = options;

    // 1. Explicit user-data-dir
    if (explicitUserDataDir) {
      return {
        userDataDir: explicitUserDataDir,
        profileType: 'explicit',
        syncPerformed: false,
        ...(profileDirectory && { profileDirectory }),
      };
    }

    // 2. Temp profile or headless-shell
    if (useTempProfile) {
      const tempDir = path.join(os.tmpdir(), `openchrome-${Date.now()}`);
      return {
        userDataDir: tempDir,
        profileType: 'temp',
        syncPerformed: false,
        ...(profileDirectory && { profileDirectory }),
      };
    }

    if (usingHeadlessShell) {
      const stableDir = path.join(os.homedir(), '.openchrome', 'headless-shell-profile');
      return {
        userDataDir: stableDir,
        profileType: 'headless-shell',
        syncPerformed: false,
        ...(profileDirectory && { profileDirectory }),
      };
    }

    // 3. Real profile available and NOT locked
    // Skip when auto-launching: Chrome 136+ rejects --remote-debugging-port with the
    // default --user-data-dir. Fall through to persistent profile with cookie sync.
    if (realProfileDir && !isProfileLocked && !isAutoLaunch) {
      return {
        userDataDir: realProfileDir,
        profileType: 'real',
        syncPerformed: false,
        ...(profileDirectory && { profileDirectory }),
      };
    }

    // 4. Real profile exists but IS locked (or auto-launch) — use persistent profile
    //    When isAutoLaunch is true, Chrome 136+ requires a non-default --user-data-dir,
    //    so we use the persistent profile even when the real profile is not locked.
    if (realProfileDir && (isProfileLocked || isAutoLaunch)) {
      const persistentDir = this.getOrCreatePersistentProfile();

      if (!this.needsSync(realProfileDir)) {
        // Persistent profile is fresh — reuse without re-sync
        return {
          userDataDir: persistentDir,
          profileType: 'persistent',
          syncPerformed: false,
          ...(profileDirectory && { profileDirectory }),
        };
      }

      // Stale — sync profile data from real profile into persistent profile
      const syncResult = this.syncProfileData(realProfileDir, persistentDir);
      return {
        userDataDir: persistentDir,
        profileType: 'persistent',
        syncPerformed: syncResult.atomic,
        ...(profileDirectory && { profileDirectory }),
      };
    }

    // 5. No real profile at all — use persistent profile (no sync needed)
    const persistentDir = this.getOrCreatePersistentProfile();
    return {
      userDataDir: persistentDir,
      profileType: 'persistent',
      syncPerformed: false,
      ...(profileDirectory && { profileDirectory }),
    };
  }

  /**
   * Remove stale lock files from a profile directory.
   * Called before launching Chrome with the persistent profile to prevent
   * degraded state after a previous force-kill (oc_stop).
   *
   * Lock files cleaned:
   * - SingletonLock, SingletonSocket, SingletonCookie (Unix)
   * - lockfile (Windows)
   *
   * Also patches Preferences to prevent "Chrome didn't shut down correctly" prompt.
   */
  cleanStaleLocks(profileDir: string, profileSubdir: string = 'Default'): void {
    const lockFiles = [
      'SingletonLock',
      'SingletonSocket',
      'SingletonCookie',
      'lockfile',
    ];

    for (const lockName of lockFiles) {
      const lockPath = path.join(profileDir, lockName);
      try {
        fs.lstatSync(lockPath);
      } catch {
        // File doesn't exist — nothing to clean
        continue;
      }
      try {
        fs.unlinkSync(lockPath);
        console.error(`[ProfileManager] Removed stale lock: ${lockPath}`);
      } catch (err) {
        console.error(`[ProfileManager] Failed to remove stale lock ${lockPath}: ${err}`);
      }
    }

    // Patch Preferences to prevent "Chrome didn't shut down correctly" restore prompt
    this.patchPreferencesExitType(profileDir, profileSubdir);
  }

  // -------------------------------------------------------------------------
  // Private helpers
  // -------------------------------------------------------------------------

  /**
   * Patch exit_type in the Default profile's Preferences file to prevent
   * Chrome's "restore pages" prompt which can block headless operation.
   */
  private patchPreferencesExitType(profileDir: string, profileSubdir: string = 'Default'): void {
    const prefsPath = path.join(profileDir, profileSubdir, 'Preferences');
    try {
      if (!fs.existsSync(prefsPath)) return;

      const raw = fs.readFileSync(prefsPath, 'utf8');
      const prefs = JSON.parse(raw);

      if (!prefs.profile) prefs.profile = {};
      prefs.profile.exit_type = 'Normal';
      prefs.profile.exited_cleanly = true;

      // Suppress session restore
      if (!prefs.session) prefs.session = {};
      prefs.session.restore_on_startup = 5;
      delete prefs.session.startup_urls;

      // Remove HMAC checksums so Chrome regenerates them on next write,
      // otherwise Chrome silently resets patched values to defaults.
      delete prefs.protection_macs;

      fs.writeFileSync(prefsPath, JSON.stringify(prefs));
      console.error('[ProfileManager] Patched Preferences: exit_type=Normal');
    } catch {
      // Parse or write failed — non-fatal, Chrome will create fresh defaults
    }
  }

  /**
   * Recursively copy a directory. Overwrites existing files.
   * Used for copying Local Storage (LevelDB) and IndexedDB directories.
   */
  private _copyDirectoryRecursive(src: string, dest: string): void {
    fs.mkdirSync(dest, { recursive: true });
    const entries = fs.readdirSync(src, { withFileTypes: true });
    for (const entry of entries) {
      const srcPath = path.join(src, entry.name);
      const destPath = path.join(dest, entry.name);
      if (entry.isDirectory()) {
        this._copyDirectoryRecursive(srcPath, destPath);
      } else {
        fs.copyFileSync(srcPath, destPath);
      }
    }
  }

  /** Check whether the `sqlite3` CLI is available on PATH. */
  private _isSqlite3Available(): boolean {
    try {
      execFileSync(
        os.platform() === 'win32' ? 'where' : 'which',
        ['sqlite3'],
        { stdio: 'ignore', timeout: 3000 },
      );
      return true;
    } catch {
      return false;
    }
  }
}
