/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';

import { packageRoot, packageJSON } from '../../package';

import type * as playwright from '../../..';

export type ClientInfo = {
  version: string;
  workspaceDirHash: string;
  daemonProfilesDir: string;
  workspaceDir: string | undefined;
  homeDir: string;
};

export function clientKey(clientInfo: ClientInfo): string {
  return clientInfo.workspaceDir || clientInfo.workspaceDirHash;
}

export type SessionConfig = {
  name: string;
  version: string;
  timestamp: number;
  socketPath: string;
  attached?: boolean;
  cli: {
    persistent?: boolean;
  };
  workspaceDir?: string;
  browser: {
    browserName: string;
    launchOptions: playwright.LaunchOptions;
    userDataDir?: string;
  };
};

export type SessionFile = {
  file: string;
  daemonDir: string;
  config: SessionConfig;
};

export class Registry {
  private _files: Map<string, SessionFile[]>;

  private constructor(files: Map<string, SessionFile[]>) {
    this._files = files;
  }

  entry(clientInfo: ClientInfo, sessionName: string): SessionFile | undefined {
    const key = clientKey(clientInfo);
    const entries = this._files.get(key) || [];
    return entries.find(entry => entry.config.name === sessionName);
  }

  entries(clientInfo: ClientInfo): SessionFile[] {
    return this._files.get(clientKey(clientInfo)) || [];
  }

  entryMap(): Map<string, SessionFile[]> {
    return this._files;
  }

  async loadEntry(clientInfo: ClientInfo, sessionName: string): Promise<SessionFile> {
    const entry = await Registry._loadSessionEntry(clientInfo.daemonProfilesDir, sessionName + '.session');
    if (!entry)
      throw new Error(`Could not start the session "${sessionName}"`);

    const key = clientKey(clientInfo);
    let list = this._files.get(key);
    if (!list) {
      list = [];
      this._files.set(key, list);
    }
    const oldIndex = list.findIndex(e => e.config.name === sessionName);
    if (oldIndex !== -1)
      list.splice(oldIndex, 1);
    list.push(entry);
    return entry;
  }

  private static async _loadSessionEntry(daemonDir: string, file: string): Promise<SessionFile | undefined> {
    try {
      const fileName = path.join(daemonDir, file);
      const data = await fs.promises.readFile(fileName, 'utf-8');
      const config = JSON.parse(data) as SessionConfig;
      // Sessions from 0.1.0 support.
      if (!config.name)
        config.name = path.basename(file, '.session');
      if (!config.timestamp)
        config.timestamp = 0;
      return { file: fileName, config, daemonDir };
    } catch {
      return undefined;
    }
  }

  static async load(): Promise<Registry> {
    const sessions = new Map<string, SessionFile[]>();
    const hashDirs = await fs.promises.readdir(baseDaemonDir).catch(() => []);
    for (const workspaceDirHash of hashDirs) {
      const daemonDir = path.join(baseDaemonDir, workspaceDirHash);
      const stat = await fs.promises.stat(daemonDir);
      if (!stat.isDirectory())
        continue;

      const files = await fs.promises.readdir(daemonDir).catch(() => []);
      for (const file of files) {
        if (!file.endsWith('.session'))
          continue;
        const entry = await Registry._loadSessionEntry(daemonDir, file);
        if (!entry)
          continue;
        const key = entry.config.workspaceDir || workspaceDirHash;
        let list = sessions.get(key);
        if (!list) {
          list = [];
          sessions.set(key, list);
        }
        list.push(entry);
      }
    }
    return new Registry(sessions);
  }
}

export const baseDaemonDir = (() => {
  if (process.env.PLAYWRIGHT_DAEMON_SESSION_DIR)
    return process.env.PLAYWRIGHT_DAEMON_SESSION_DIR;

  let localCacheDir: string | undefined;
  if (process.platform === 'linux')
    localCacheDir = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
  if (process.platform === 'darwin')
    localCacheDir = path.join(os.homedir(), 'Library', 'Caches');
  if (process.platform === 'win32')
    localCacheDir = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
  if (!localCacheDir)
    throw new Error('Unsupported platform: ' + process.platform);
  return path.join(localCacheDir, 'ms-playwright', 'daemon');
})();

export function createClientInfo(): ClientInfo {
  const workspaceDir = findWorkspaceDir(process.cwd());
  const version = process.env.PLAYWRIGHT_CLI_VERSION_FOR_TEST || packageJSON.version;

  const hash = crypto.createHash('sha1');
  hash.update(workspaceDir || packageRoot);
  const workspaceDirHash = hash.digest('hex').substring(0, 16);

  return {
    version,
    workspaceDir,
    workspaceDirHash,
    daemonProfilesDir: daemonProfilesDir(workspaceDirHash),
    homeDir: os.homedir(),
  };
}

function findWorkspaceDir(startDir: string): string | undefined {
  let dir = startDir;
  for (let i = 0; i < 10; i++) {
    if (fs.existsSync(path.join(dir, '.playwright')))
      return dir;
    const parentDir = path.dirname(dir);
    if (parentDir === dir)
      break;
    dir = parentDir;
  }
  return undefined;
}

const daemonProfilesDir = (workspaceDirHash: string) => {
  return path.join(baseDaemonDir, workspaceDirHash);
};

export function explicitSessionName(sessionName?: string): string | undefined {
  return sessionName || process.env.PLAYWRIGHT_CLI_SESSION;
}

export function resolveSessionName(sessionName?: string): string {
  return explicitSessionName(sessionName) || 'default';
}
