/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * 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 type {
  CancellationToken,
  DebugConfiguration,
  ProviderResult,
  WorkspaceFolder,
} from 'vscode';
import * as vscode from 'vscode';
import { exec, execSync, spawn } from 'child_process';
import path from 'path';
import * as fs from 'fs';
import { execNativeCommand } from './NewWebviewProvider';
import { config } from 'process';

export class KeelsDebugAdapterConfigProvider {
  private static deviceSerialNum: string;
  private static bundleName: string;
  private static remotePlatform: string = '';
  private static plateformSocketName: string;

  private static readonly NDK_REQUIRED_VERSION = '26.3.11579264';
  private static readonly CLANG_VERSION = "17";
  private static readonly MAX_PID_ATTEMPTS = 10;
  private static readonly PID_RETRY_DELAY = 300;

  private static generatePlateformSocketName(): void {
    let socketNum = Date.now().toString();
    let socketDir = this.bundleName;
    if (socketDir !== '') {
      socketDir = `/${socketDir}`;
    }
    if (this.remotePlatform === 'remote-ohos') {
      this.plateformSocketName = `${socketDir}/platform-${socketNum}.sock`;
    } else {
      this.plateformSocketName = `${socketDir}/lldb_debug-${socketNum}.sock`;
    }
  }

  private static resetConfigInfo(config: DebugConfiguration) {
    this.deviceSerialNum = config.deviceSerialNum;
    this.bundleName = config.bundleName;
    this.generatePlateformSocketName();
    this.remotePlatform = config.remotePlatform;
    config.processId = '';
    if (config.afterConnectCommands === undefined) {
      config.afterConnectCommands = [];
    }
    for (let element of config.lib_path) {
      config.afterConnectCommands.push(`settings append target.exec-search-paths \"${element}\"`);
    }
    if (config.postAttachCommands === undefined) {
      config.postAttachCommands = ["process handle -s false SIGSEGV"];
      for (let element of config.source_map) {
        config.postAttachCommands.push(
          `settings set target.source-map ${element[0]} ${element[1].toLowerCase()}`
        );
      }
    } else {
      config.postAttachCommands.push("process handle -s false SIGSEGV");
      for (let element of config.source_map) {
        config.postAttachCommands.push(
          `settings set target.source-map ${element[0]} ${element[1].toLowerCase()}`
        );
      }
    }
    if (config.remotePlatform === 'remote-ohos') {
      config.remoteAddress = `unix-abstract-connect://[${this.deviceSerialNum}]${this.plateformSocketName}`;
    } else {
      config.remoteAddress = `unix-abstract-connect://[${this.deviceSerialNum}]/data/user/0${this.plateformSocketName}`;
    }

    if (config.startupCommands === undefined) {
      config.startupCommands = ["settings set auto-confirm true"];
    } else {
      config.startupCommands.push("settings set auto-confirm true");
    }

    delete config.lib_path;
    delete config.source_map;
  }

  private static getLLDBForOhosPath(): string {
    const platform = process.platform;
    const sdkRoot = process.env.DEVECO_SDK_HOME;
    if (!sdkRoot) {
      throw new Error('DEVECO_SDK_HOME environment variable is not set.');
    }

    if (platform === 'win32' || platform === 'darwin') {
      const lldbPath = path.join(sdkRoot, 'default', 'hms', 'native', 'lldb', 'aarch64-linux-ohos', 'lldb-server');
      if (fs.existsSync(lldbPath)) {
        return lldbPath;
      }
      throw new Error(
        `lldb-server not found at ${lldbPath}. Please ensure:\n` +
        `DEVECO_SDK_HOME is set correctly (current: ${sdkRoot})`
      );
    } else {
      throw new Error(`Unsupported platform: ${platform}`);
    }
  }

  private static getLLDBForAndroidPath(): string {
    const platform = process.platform;
    const sdkRoot = process.env.ANDROID_SDK_ROOT;
    if (!sdkRoot) {
      throw new Error('ANDROID_SDK_ROOT environment variable is not set.');
    }

    const ndkPath = path.join(sdkRoot, 'ndk', this.NDK_REQUIRED_VERSION);
    if (!fs.existsSync(ndkPath)) {
      throw new Error(
        `NDK version ${this.NDK_REQUIRED_VERSION} not found at ${ndkPath}. ` +
        `Please install the required NDK version via SDK Manager or set ANDROID_SDK_ROOT correctly.`
      );
    }

    let systemDir: string;
    if (platform === 'win32') {
      systemDir = 'windows-x86_64';
    } else if (platform === 'darwin') {
      systemDir = 'darwin-x86_64';
    } else {
      throw new Error(`Unsupported platform: ${platform}`);
    }

    const lldbPath = path.join(
      ndkPath,
      'toolchains',
      'llvm',
      'prebuilt',
      systemDir,
      'lib',
      'clang',
      this.CLANG_VERSION,
      'lib',
      'linux',
      'aarch64',
      'lldb-server'
    );

    if (fs.existsSync(lldbPath)) {
      return lldbPath;
    }

    throw new Error(
      `lldb-server not found at ${lldbPath}. Please ensure:\n` +
      `ANDROID_SDK_ROOT is set correctly (current: ${sdkRoot})`
    );
  }

  private static getAdbPath(): string {
    const isWindows = process.platform === 'win32';
    const adbExe = isWindows ? 'adb.exe' : 'adb';

    if (!this.deviceSerialNum) {
      throw new Error('deviceSerialNum is not set before calling getAdbPath');
    }

    try {
      const cmd = isWindows ? `where ${adbExe}` : `which ${adbExe}`;
      const pathResult = execSync(cmd, { encoding: 'utf-8' }).split('\n')[0];
      if (pathResult?.trim()) {
        // Escape spaces in the path
        const escapedPath = isWindows ? `"${pathResult.trim()}"` : pathResult.trim().replace(/ /g, '\\ ');
        return `${escapedPath} -s ${this.deviceSerialNum}`;
      }
    } catch {
      //do noting
    }

    const sdkRoot = process.env.ANDROID_SDK_ROOT;
    if (sdkRoot) {
      const sdkPath = path.join(sdkRoot, 'platform-tools', adbExe);
      if (fs.existsSync(sdkPath)) {
        // Escape spaces in the path
        const escapedSdkPath = isWindows ? `"${sdkPath}"` : sdkPath.replace(/ /g, '\\ ');
        return `${escapedSdkPath} -s ${this.deviceSerialNum}`;
      }
    }

    throw new Error(
      `Could not find ${adbExe} executable. Please try either;\n` +
      `1. Add adb to system PATH environment variable.\n` +
      `2. Set ANDROID_SDK_ROOT enviroment variable to your SDK root directory.`,
    );
  }

  private static getHdcPath(): string {
    const isWindows = process.platform === 'win32';
    const hdcExe = isWindows ? 'hdc.exe' : 'hdc';

    try {
      const cmd = isWindows ? `where ${hdcExe}` : `which ${hdcExe}`;
      const pathResult = execSync(cmd, { encoding: 'utf-8' }).split('\n')[0];
      if (pathResult?.trim()) {
        return pathResult.trim() + ` -t ${this.deviceSerialNum}`;
      }
    } catch {
      //do noting
    }

    const sdkRoot = process.env.DEVECO_SDK_HOME;
    if (sdkRoot) {
      const pathsToCheck = [
        path.join(sdkRoot, 'toolchains', hdcExe),
        path.join(sdkRoot, 'default', 'openharmony', 'toolchains', hdcExe),
      ];
      for (const p of pathsToCheck) {
        if (fs.existsSync(p)) {
          return p + ` -t ${this.deviceSerialNum}`;
        }
      }
    }
    throw new Error(
      `Could not find ${hdcExe} executable. Please try either;\n` +
      `1. Add hdc to system PATH enviroment variable.\n` +
      `2. Set DEVECO_SDK_HOME enviroment variable to your SDK root directory.`,
    );
  }

  private static async pushLldbserverInAndroid() {
    const adbCmd = this.getAdbPath();
    const lldbServer = this.getLLDBForAndroidPath().replace(/\\/g, '/');
    const lldbServerDir = path.join('/data/local/tmp').replace(/\\/g, '/');
    const lldbServerInDevice = path.join(lldbServerDir, 'lldb-server').replace(/\\/g, '/');

    const lldbServerPushCmd = `${adbCmd} push ${lldbServer} ${lldbServerInDevice}`;
    const chmodInDevice = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'chmod +x lldb-server'"`;
    const stopLldbserverIfRunning = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'pkill -9 lldb-server || true'"`;
    const removeLldbserverIfExists = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'if [ -e lldb-server ]; then rm -f lldb-server; fi'"`;
    const cpLldbserver = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'cp ${lldbServerInDevice} .'"`;

    await execNativeCommand(lldbServerPushCmd).catch((err) =>
      console.error('push lldb-server to device failed:', err)
    );
    await execNativeCommand(stopLldbserverIfRunning).catch((err) =>
      console.error(`stop running lldb-server in ${this.bundleName} failed:`, err)
    );
    await execNativeCommand(removeLldbserverIfExists).catch((err) =>
      console.error(`remove existing lldb-server in ${this.bundleName} failed:`, err)
    );
    await execNativeCommand(cpLldbserver).catch((err) =>
      console.error(`cp lldb-server to ${this.bundleName} failed:`, err)
    );
    await execNativeCommand(chmodInDevice).catch((err) =>
      console.error('chmod lldb-server to device failed:', err)
    );
  }

  private static async pushLldbserverInOhos() {
    const hdcCmd = this.getHdcPath();
    const lldbServer = this.getLLDBForOhosPath();
    const lldbServerDir = path
      .join('/data/local/tmp/debugserver', this.bundleName)
      .replace(/\\/g, '/');
    const lldbServerPathInDevice = path.join(lldbServerDir, 'lldb-server').replace(/\\/g, '/');

    const makeDir = `${hdcCmd} shell mkdir -p ${lldbServerDir}`;
    const chmodInPC = `${hdcCmd} shell chmod 757 ${lldbServerDir}`;

    const lldbServerPushCmd = `${hdcCmd} file send "${lldbServer}" "${lldbServerPathInDevice}"`;
    const chmodInDevice = `${hdcCmd} shell chmod 755 ${lldbServerPathInDevice}`;

    await execNativeCommand(makeDir).catch((err) =>
      console.error('make dir in device failed:', err),
    );
    await execNativeCommand(chmodInPC).catch((err) =>
      console.error('chmod dir in device failed:', err),
    );
    await execNativeCommand(lldbServerPushCmd).catch((err) =>
      console.error('push lldb-server to device failed:', err),
    );
    await execNativeCommand(chmodInDevice).catch((err) =>
      console.error('chmod lldb-server to device failed:', err),
    );
  }

  static async sleep(milliseconds: number): Promise<void> {
    return new Promise((f) => setTimeout(f, milliseconds));
  }

  private static async startLldbServer(name: string) {
    const hdcCmd = this.getHdcPath();
    const isWindows = process.platform === 'win32';
    let startDebug = `${hdcCmd} shell aa process -a EntryAbility -b ${name} -D "/data/local/tmp/debugserver/${name}/lldb-server platform --listen unix-abstract://${this.plateformSocketName} --log-channels 'lldb process:gdb-remote packets' --log-file '/data/local/tmp/debugserver/${name}/platform.log'"`;
    execSync(startDebug, { shell: isWindows ? 'powershell.exe' : '/bin/bash' });
  }

  private static async checkAndroidLLDBServerStartUp(config: DebugConfiguration): Promise<string> {
    const adbCmd = this.getAdbPath();
    const isWindows = process.platform === 'win32';
    let exePath = isWindows
      ? `${adbCmd} shell ps -ef | findstr ${config.bundleName}`
      : `${adbCmd} shell ps -ef | grep ${config.bundleName}`;
    let cmdResultInfo = await execNativeCommand(exePath);
    if (cmdResultInfo.includes(this.plateformSocketName)) {
      return 'success';
    }
    return 'failed';
  }

  private static async startAndroidLldbServer(config: DebugConfiguration): Promise<string> {
    const adbCmd = this.getAdbPath();
    let startCmd = `${adbCmd} shell "run-as ${config.bundleName} sh -c './lldb-server platform --server --listen unix-abstract:///data/user/0${this.plateformSocketName} </dev/null >/dev/null 2>&1 &'"`;
    execNativeCommand(startCmd).catch((err) => console.error('First command failed:', err));
    await this.sleep(3000);
    return this.checkAndroidLLDBServerStartUp(config);
  }

  private static parsePidofResult(output: string): string {
    const pidMatch = output
      .trim()
      .split(/\s+/)
      .find((part) => /^\d+$/.test(part));
    return pidMatch ?? '';
  }

  private static async initAndroidPid(config: DebugConfiguration) {
    await this.startAndroidLldbServer(config);
    const adbCmd = this.getAdbPath();

    let attemptCount = 0;
    let result: string = '';
    while (attemptCount < this.MAX_PID_ATTEMPTS) {
      try {
        const pidofResult = await execNativeCommand(`${adbCmd} shell pidof ${config.bundleName}`);
        result = this.parsePidofResult(pidofResult);
      } catch (err) {
        console.info(`pidof lookup failed for ${config.bundleName}:`, err);
      }

      if (result !== '') {
        config.processId = result;
        break;
      }

      if (attemptCount < this.MAX_PID_ATTEMPTS) {
        console.info(
          `Try to attach application. Attempt ${attemptCount} times failed, retrying...`,
        );
        await this.sleep(this.PID_RETRY_DELAY);
      }
      attemptCount++;
    }
    if (result.length > 0) {
      vscode.window.showInformationMessage('Start a process successfully!');
    } else {
      vscode.window.showInformationMessage('Failed to start process.');
    }
  }

  private static async initOHPid(config: DebugConfiguration) {
    await this.startLldbServer(config.bundleName);
    const hdcCmd = this.getHdcPath();

    try {
      const attachCmd = `${hdcCmd} shell aa attach -b ${config.bundleName}`;
      await execNativeCommand(attachCmd);
      console.log(`successfully attached to bundle: ${config.bundleName}`);
      await this.sleep(this.PID_RETRY_DELAY);
    } catch (err) {
      console.error('Failed to attach to application:', err);
      vscode.window.showErrorMessage(`Failed to attach to ${config.bundleName}: ${err}`);
      return;
    }

    let attemptCount = 0;
    let result: string = '';
    while (attemptCount < this.MAX_PID_ATTEMPTS) {
      const isWindows = process.platform === 'win32';
      let cmdResultInfo = isWindows
        ? await execNativeCommand(`${hdcCmd} shell ps -ef | findstr ${config.bundleName}`)
        : await execNativeCommand(`${hdcCmd} shell ps -ef | grep ${config.bundleName}`);
      const lines = cmdResultInfo.split('\n');
      if (cmdResultInfo.includes('process:gdb-remote')) {
        lines.forEach((line) => {
          const parts = line.trim().split(/\s+/);
          if (parts.length >= 4 && parts[7].toLowerCase() === config.bundleName) {
            result = parts[1];
          }
        });
      }
      if (result !== '') {
        config.processId = result.toString();
        break;
      }
      if (attemptCount < this.MAX_PID_ATTEMPTS) {
        console.info(
          `Try to attach application. Attempt ${attemptCount} times failed, retrying...`,
        );
        await this.sleep(this.PID_RETRY_DELAY);
      }
      attemptCount++;
    }
    if (result.length > 0) {
      vscode.window.showInformationMessage('Start a process successfully!');
    } else {
      vscode.window.showInformationMessage('Failed to start process.');
    }
  }

  public static async resolveDebugConfigAsync(
    configuration: DebugConfiguration,
  ): Promise<DebugConfiguration> {
    const config = configuration;
    this.resetConfigInfo(config);
    if (config.remotePlatform === 'remote-ohos') {
      await this.pushLldbserverInOhos();
      await this.initOHPid(config);
    } else if (config.remotePlatform === 'remote-android') {
      await this.pushLldbserverInAndroid();
      await this.initAndroidPid(config);
    }
    return config;
  }
}