/*
 * 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 * as vscode from 'vscode';
import { exec, execSync, spawn } from 'child_process';
import { debounce } from 'lodash';
import path from 'path';
import * as fs from 'fs';
import * as ini from 'ini';

export type PlatformType = 'android' | 'ohos' | string;
export type Category = 'mobile' | string;
// 3 minutes
export const TimeDiff = 180000;

const PLATFORM_MAPPING: Record<string, { category: Category; platform: string }> = {
  android: { category: 'mobile', platform: 'Android' },
  ohos: { category: 'mobile', platform: 'HarmonyOS' },
};

/**
 * Set the path of CJMP SDK
 */
export async function setCjmpSdkPath(sdkPath: string): Promise<void> {
  try {
    if (!sdkPath) {
      vscode.window.showErrorMessage('CJMP SDK path not set. Operation cancelled.');
      return;
    }

    const config = vscode.workspace.getConfiguration();
    await config.update('CJMP.sdkPath', sdkPath, vscode.ConfigurationTarget.Global);
  } catch (error) {
    vscode.window.showErrorMessage(
      `Failed to set CJMP SDK path: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Get the path of CJMP SDK
 * @returns the path of CJMP SDK or undefined
 */
export async function getCjmpSdkPath(): Promise<string | undefined> {
  const config = vscode.workspace.getConfiguration();
  const cjmpSdkPath = config.get<string>('CJMP.sdkPath');

  if (!cjmpSdkPath) {
    vscode.window.showErrorMessage(
      `CJMP SDK path is not set. Please configure "CJMP.sdkPath" in your settings.`,
    );
    return undefined;
  }

  return cjmpSdkPath;
}

/**
 * Validate if the CJMP SDK path is valid
 * @param cjmpSdkPath The SDK path to validate
 * @returns Returns true if the SDK path is valid, otherwise false
 */
export function validateCjmpSdkPath(cjmpSdkPath?: string): boolean {
  const sdkPath = cjmpSdkPath || vscode.workspace.getConfiguration().get<string>('CJMP.sdkPath');

  if (!sdkPath) {
    vscode.window.showErrorMessage('CJMP SDK path is not configured in settings. Please set "CJMP.sdkPath".');
    return false;
  }

  if (!sdkPath.endsWith('bin')) {
    vscode.window.showErrorMessage(`CJMP SDK validation failed for path: ${sdkPath}`);
    return false;
  }

  const cjmpToolsDir = path.dirname(sdkPath);

  const thirdPartyPath = path.join(cjmpToolsDir, 'third_party');
  if (!fs.existsSync(thirdPartyPath)) {
    vscode.window.showErrorMessage(`CJMP SDK validation failed: The "third_party" folder was not found in ${cjmpToolsDir}.`);
    return false;
  }

  const toolsPath = path.join(cjmpToolsDir, 'tools')
  if (!fs.existsSync(toolsPath)) {
    vscode.window.showErrorMessage(`CJMP SDK validation failed: The "tools" folder not found in ${cjmpToolsDir}.`);
    return false;
  }

  return true;
}


export interface Device {
  name: string;
  id: string;
  isSupported: boolean;
  targetPlatform: string;
  emulator: boolean;
  sdk?: string;
  category: Category;
  platform: string;
}

/**
 * Execute "keels devices --machine" to obtain the device list in JSON format.
 */
export class KeelsDeviceManager {
  private statusBarItem: vscode.StatusBarItem;
  public currentDevice: Device | null = null;
  private context: vscode.ExtensionContext;

  constructor(context: vscode.ExtensionContext) {
    this.context = context;

    this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
    this.statusBarItem.command = 'cjmp.selectDevice';
    this.statusBarItem.tooltip = 'Select CJMP Device';

    this.loadPersistedDevice();

    this.startDeviceMonitoring();
  }

  private startDeviceMonitoring() {
    this.startPolling();
  }

  private startPolling(intervalMs: number = 10000) {
    setInterval(() => this.debouncedRefreshDevices(), intervalMs);
  }

  // Anti-shake refresh (to avoid frequent calls)
  private debouncedRefreshDevices = debounce(() => this.refreshDevices(), 500);

  public async getKeelsDevices(): Promise<Device[]> {
    return new Promise((resolve) => {
      const cjmpSdkPath = vscode.workspace.getConfiguration().get<string>('CJMP.sdkPath');
      if (!cjmpSdkPath) {
        return;
      }

      const isWindows = process.platform === 'win32';
      const command = isWindows
        ? `${path.join(cjmpSdkPath, 'keels')} devices --machine`
        : `${path.join(cjmpSdkPath, 'keels')} devices --machine`.replace(/\\/g, '/');

      exec(command, { shell: isWindows ? 'cmd.exe' : '/bin/bash' }, (error, stdout) => {
        if (error) {
          resolve([]);
        } else {
          try {
            const rawData: any[] = JSON.parse(stdout);
            const devices = rawData.map((item) => {
              const { category, platformName } = this.parsePlatform(item.targetPlatform || '');
              return {
                name: item.name || 'Unknown Device',
                id: item.id || '',
                isSupported: !!item.isSupported,
                targetPlatform: item.targetPlatform || 'unknown',
                emulator: !!item.emulator,
                sdk: item.sdk,
                category,
                platform: platformName,
              } as Device;
            });
            resolve(devices);
          } catch (e) {
            resolve([]);
          }
        }
      });
    });
  }

  private parsePlatform(targetPlatform: string) {
    const [basePlatform, ...architectureParts] = targetPlatform.split(/[-_]/);

    const mapping = PLATFORM_MAPPING[basePlatform.toLowerCase()] || {
      category: 'unknown' as Category,
      platform: basePlatform,
    };

    return {
      category: mapping.category,
      platformName: mapping.platform,
    };
  }

  private async refreshDevices() {
    const devices = await this.getKeelsDevices();
    if (devices.length === 0) {
      this.statusBarItem.text = '$(alert) No Devices';
      this.statusBarItem.show();
      return;
    }

    const currentDeviceExists =
      this.currentDevice && devices.some((d) => d.id === this.currentDevice?.id);

    if (!currentDeviceExists) {
      const savedDeviceId = this.context.workspaceState.get<string>('keels.selectedDevice');
      const savedDevice = devices.find((d) => d.id === savedDeviceId);
      this.setCurrentDevice(savedDevice || devices[0]);
    }
  }

  private loadPersistedDevice() {
    const savedDeviceId = this.context.workspaceState.get<string>('keels.selectedDevice');
    if (savedDeviceId) {
      this.statusBarItem.text = '$(loading~spin) Loading...';
      this.statusBarItem.show();
    }
  }

  private saveCurrentDevice(device: Device) {
    this.context.workspaceState.update('keels.selectedDevice', device.id);
  }

  private setCurrentDevice(device: Device) {
    this.currentDevice = device;
    this.statusBarItem.text = `$(device-mobile) ${device.name} (${device.targetPlatform})`;
    this.statusBarItem.show();
    this.saveCurrentDevice(device);
  }

  public async showDevicePicker(): Promise<Device | undefined> {
    const devices = await this.getKeelsDevices();
    if (devices.length === 0) {
      vscode.window.showInformationMessage('No CJMP devices found.');
      return;
    }

    const picked = await vscode.window.showQuickPick(
      devices.map((d) => ({
        label: this.labelForDevice(d, { withIcon: true }),
        description: `${d.id} - ${d.category || d.platform || ''}`,
        detail: d === this.currentDevice ? 'Current Device' : undefined,
        device: d,
      })),
      { placeHolder: 'Select a CJMP device' },
    );

    if (picked) {
      this.setCurrentDevice(picked.device);
    }

    return picked?.device;
  }

  public labelForDevice(device: Device, { withIcon = false }: { withIcon?: boolean } = {}): any {
    let icon;
    switch (device.category) {
      case 'mobile':
        icon = '$(device-mobile) ';
        break;
      default:
        icon = undefined;
    }
    const name = device.name;
    return withIcon ? `${icon ?? ''}${name}` : name;
  }

  public dispose() {
    this.statusBarItem.dispose();
  }
}

export function getIDEType(): string {
  return vscode.env.appName;
}

export function getWorkSpacePath() {
  const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
  if (!workspaceFolder) {
    vscode.window.showErrorMessage('No workspace folder found.');
    return;
  }
  return workspaceFolder.uri;
}

export async function getBundleName() {
  const projectName = await getProjectName();
  const orgName = await getOrgName();
  if (!orgName) {
    return '';
  }

  if (!projectName) {
    return '';
  }
  return `${orgName}.${projectName}`;
}

export async function getProjectName() {
  let projectConf = getProjectConf();
  if (!projectConf) {
    return;
  }

  const fileContent = fs.readFileSync(projectConf, 'utf-8');
  const config = ini.parse(fileContent);
  return config.project.name;
}

export async function getOrgName() {
  let projectConf = getProjectConf();
  if (!projectConf) {
    return;
  }

  const fileContent = fs.readFileSync(projectConf, 'utf-8');
  const config = ini.parse(fileContent);
  return config.project.organization;
}

export async function getPackageName() {
  let projectConf = getProjectConf();
  if (!projectConf) {
    return;
  }


  const fileContent = fs.readFileSync(projectConf, 'utf-8');
  const config = ini.parse(fileContent);
  return config.project.packageName;
}

export async function getLibPath(remotePlatform: string | undefined) {
  const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
  if (!workspaceFolder) {
    vscode.window.showErrorMessage('No workspace folder found.');
    return;
  }

  let hosPath = vscode.Uri.joinPath(
    workspaceFolder.uri,
    process.platform === 'win32' ? 'hos\\entry\\build\\default\\intermediates\\libs\\default\\arm64-v8a' : 'hos/entry/build/default/intermediates/libs/default/arm64-v8a',
  );

  const toWorkspacePath = (relativePath: string) =>
    vscode.Uri.joinPath(
      workspaceFolder.uri,
      process.platform === 'win32' ? relativePath : relativePath.replace(/\\/g, '/')
    ).fsPath;

  // AGP 7.x/8.x generate different intermediate native lib directories.
  // Keep both candidates so debugger symbol loading works across template versions.
  const androidCandidates = [
    'android\\app\\build\\intermediates\\merged_native_libs\\debug\\out\\lib\\arm64-v8a',
    'android\\app\\build\\intermediates\\merged_native_libs\\debug\\mergeDebugNativeLibs\\out\\lib\\arm64-v8a',
  ];

  const hosPathExists = fs.existsSync(hosPath.fsPath);
  let androidLib = androidCandidates
    .map((candidate) => toWorkspacePath(candidate))
    .filter((candidatePath, index, paths) => fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index);

  let hosLib = [hosPath.fsPath];

  if (getProjectType() === 'logic-module') {
    const projectName = await getPackageName();
    const logicModuleCandidates = [
      `android\\${projectName}\\build\\intermediates\\merged_native_libs\\debug\\out\\lib\\arm64-v8a`,
      `android\\${projectName}\\build\\intermediates\\merged_native_libs\\debug\\mergeDebugNativeLibs\\out\\lib\\arm64-v8a`,
    ];
    androidLib.push(
      ...logicModuleCandidates
        .map((candidate) => toWorkspacePath(candidate))
        .filter((candidatePath, index, paths) => fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index)
    );
    androidLib = androidLib.filter((candidatePath, index, paths) => paths.indexOf(candidatePath) === index);
  }

  switch (remotePlatform) {
    case 'remote-ohos':
      return hosPathExists ? hosLib : [];
    case 'remote-android':
      return androidLib;
    default:
      return [];
  }
}

export const cjmpDoctorWithSpawn = async (): Promise<void> => {
  return new Promise((resolve, reject) => {
    try {
      getCjmpSdkPath().then((cjmpSdkPath) => {
        if (!cjmpSdkPath) {
          vscode.window.showErrorMessage(
            'CJMP SDK path is not configured. Please set it up first.',
          );
          reject(new Error('CJMP SDK path is not configured.'));
          return;
        }

        const command = process.platform === 'win32' ? 'keels doctor -v' : './keels doctor -v';
        const outputChannel = vscode.window.createOutputChannel('Keels Doctor');
        outputChannel.show(true);
        outputChannel.append('Running keels doctor -v ...\n');

        const doctorProcess =
          process.platform === 'win32'
            ? spawn('cmd', ['/c', command], { cwd: cjmpSdkPath })
            : spawn(getDefaultShell(), ['-c', command], { cwd: cjmpSdkPath.replace(/\\/g, '/') });

        doctorProcess.stdout.on('data', (data) => {
          const message = data.toString();
          outputChannel.append(message);
          console.log(message);
        });

        doctorProcess.stderr.on('data', (data) => {
          const errorMessage = data.toString();
          outputChannel.append(errorMessage);
          console.log(errorMessage);
        });

        doctorProcess.on('close', async (code) => {
          if (code === 0) {
            vscode.window.showInformationMessage('Keels doctor cammand executed successfully!');
            resolve();
          } else {
            vscode.window.showErrorMessage(
              `Failed to execute "keels doctor -v". Exit code: ${code}`,
            );
            reject(new Error(`Failed to execute "keels doctor -v". Exit code: ${code}`));
          }
        });

        doctorProcess.on('error', (error) => {
          vscode.window.showErrorMessage(`Failed to excute command: ${error.message}`);
          outputChannel.appendLine(`Error: ${error.message}`);
          reject(error);
        });
      });
    } catch (error) {
      vscode.window.showErrorMessage(
        `Error running keels doctor: ${error instanceof Error ? error.message : String(error)}`,
      );
      reject(error);
    }
  });
};

export function getAdbPath(): string {
  const isWindows = process.platform === 'win32';
  const adbExe = isWindows ? 'adb.exe' : 'adb';
  try {
    const cmd = isWindows ? `where.exe ${adbExe}` : `which ${adbExe}`;
    const pathResult = execSync(cmd, { encoding: 'utf-8' }).split('\n')[0];
    if (pathResult?.trim()) {
      return pathResult.trim();
    }
  } catch {
    vscode.window.showErrorMessage('find env var for adb faild');
  }
  const sdkRoot = process.env.ANDROID_SDK_ROOT;
  if (sdkRoot) {
    const sdkPath = path.join(sdkRoot, 'platform-tools', adbExe);
    if (fs.existsSync(sdkPath)) {
      return sdkPath;
    }
  }
  vscode.window.showErrorMessage(
    `Could not find ${adbExe} executable. Please try either:\n` +
    `1. Add adb to system Path env varible:\n` +
    `2. Set ANDROID_SDK_ROOT environment variable to your SDK root directory.`,
  );
  return '';
}

export function getDefaultShell(): string {
  const defaultShell = process.platform === "darwin" ? "/bin/zsh" : "/bin/bash";
  return process.env.SHELL ?? defaultShell;
}

export function getProjectConf(): string | undefined {
  const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
  if (!workspaceFolder) {
    return '';
  }

  let projectConf = vscode.Uri.joinPath(workspaceFolder.uri, 'project.conf');
  if (!fs.existsSync(projectConf.fsPath)) {
    return '';
  }

  return projectConf.fsPath;
}

export function getProjectType(): string | undefined {
  let projectConf = getProjectConf();
  if (!projectConf) {
    return;
  }

  const fileContent = fs.readFileSync(projectConf, 'utf-8');
  const config = ini.parse(fileContent);
  return config.project.type;
}

let stripAnsi: (str: string) => string;

export async function activateStripAnsi() {
  stripAnsi = (await import('strip-ansi')).default;
}

// Delete ANSI color code from output
const _orig = (vscode.window.createOutputChannel) as any;
vscode.window.createOutputChannel = function (
  name: string,
  options?: string | { log: true }
) {
  const ch = _orig(name, options);
  if (!('append' in ch)) return ch;

  const _append = ch.append.bind(ch);
  const _appendLine = ch.appendLine.bind(ch);
  ch.append = (val: string) => _append(stripAnsi(val));
  ch.appendLine = (val: string) => _appendLine(stripAnsi(val));
  return ch;
};