/*
 * 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 * as os from 'os';
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';
import { OS, RemotePlatform } from './types';
import {resolveAndWriteEnvVars} from "./envResolver";
import {
  resolveCjmpSdkRoot,
  resolveKeelsCommand,
} from './cjmpSdk';

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' },
  ios: { category: 'mobile', platform: 'iOS' },
};

/** Stores the CJMP SDK root in the requested configuration scope. */
export async function setCjmpSdkRoot(
  sdkRoot: string,
  configurationTarget: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
): Promise<void> {
  try {
    if (!sdkRoot) {
      vscode.window.showErrorMessage('CJMP SDK root not set. Operation cancelled.');
      return;
    }

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


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

interface KeelsDevicesEnvelope {
  devices?: unknown[];
  diagnostics?: unknown[];
}

/**
 * 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;
  private deviceMonitoringInterval: ReturnType<typeof setInterval> | undefined;
  private readonly isCjmpProject: boolean;

  constructor(context: vscode.ExtensionContext, isCjmpProject: boolean = true) {
    this.context = context;
    this.isCjmpProject = isCjmpProject;

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

    if (this.isCjmpProject) {
      this.loadPersistedDevice();
      this.deviceMonitoringInterval = this.startDeviceMonitoring();
    }
  }

  private startDeviceMonitoring(): ReturnType<typeof setInterval> {
    return this.startPolling();
  }

  private startPolling(intervalMs: number = 10000): ReturnType<typeof setInterval> {
    return 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 keelsCommand = resolveKeelsCommand({ reportErrors: false });
      if (!keelsCommand) {
        resolve([]);
        return;
      }

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

      exec(command, {
        shell: isWindows ? 'cmd.exe' : '/bin/bash',
        env: keelsCommand.environment,
      }, (error, stdout) => {
        if (error) {
          resolve([]);
        } else {
          try {
            const rawData = this.extractDeviceRecords(stdout);
            const devices = rawData.map((item: any) => {
              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 extractDeviceRecords(stdout: string): any[] {
    // Newer keels returns an envelope with diagnostics; older versions returned a bare array.
    // Accept both so device discovery keeps working across SDK revisions.
    const parsed = JSON.parse(stdout) as unknown;

    if (Array.isArray(parsed)) {
      return parsed;
    }

    const envelope = parsed as KeelsDevicesEnvelope;
    if (Array.isArray(envelope.devices)) {
      return envelope.devices;
    }

    throw new Error('Unsupported keels devices payload');
  }

  private async refreshDevices() {
    const devices = await this.getKeelsDevices();
    if (devices.length === 0) {
      this.statusBarItem.text = '$(alert) No Devices';
      this.showStatusBarItem();
      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.showStatusBarItem();
    }
  }

  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.showStatusBarItem();
    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;
  }

  private showStatusBarItem() {
    if (this.isCjmpProject) {
      this.statusBarItem.show();
    }
  }

  public dispose() {
    if (this.deviceMonitoringInterval !== undefined) {
      clearInterval(this.deviceMonitoringInterval);
    }
    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 function getExampleDirectory(): string | undefined {
  const projectConf = getProjectConf();
  if (!projectConf) {
    return;
  }

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

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

  const workspaceRoot = workspaceFolder.uri.fsPath;
  const projectType = getProjectType();
  const exampleDirectory = projectType === 'library' ? getExampleDirectory() : undefined;
  const platformProjectRoot = exampleDirectory
    ? path.join(workspaceRoot, exampleDirectory)
    : workspaceRoot;

  const hosLibraryPath = path.join(
    platformProjectRoot,
    'hos',
    'entry',
    'build',
    'default',
    'intermediates',
    'libs',
    'default',
    'arm64-v8a',
  );

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

  let androidLibraryPaths = androidLibraryCandidates
    .filter((candidatePath, index, paths) => fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index);

  const iosLibraryCandidates = getOs() === 'mac'
    ? [
        path.join(platformProjectRoot, 'build', 'aarch64-apple-ios', 'debug'),
        ...(projectType === 'library' && platformProjectRoot !== workspaceRoot
          ? [path.join(workspaceRoot, 'build', 'aarch64-apple-ios', 'debug')]
          : []),
      ]
    : [];
  const iosLibraryPaths = iosLibraryCandidates.filter(
    (candidatePath, index, paths) =>
      fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index,
  );

  if (projectType === 'logic-module') {
    const packageName = await getPackageName();
    const logicModuleCandidates = [
      path.join(
        workspaceRoot,
        'android',
        packageName,
        'build',
        'intermediates',
        'merged_native_libs',
        'debug',
        'out',
        'lib',
        'arm64-v8a',
      ),
      path.join(
        workspaceRoot,
        'android',
        packageName,
        'build',
        'intermediates',
        'merged_native_libs',
        'debug',
        'mergeDebugNativeLibs',
        'out',
        'lib',
        'arm64-v8a',
      ),
    ];
    androidLibraryPaths.push(
      ...logicModuleCandidates
        .filter((candidatePath, index, paths) => fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index)
    );
    androidLibraryPaths = androidLibraryPaths.filter(
      (candidatePath, index, paths) => paths.indexOf(candidatePath) === index,
    );
  }

  switch (remotePlatform) {
    case RemotePlatform.OHOS:
      return fs.existsSync(hosLibraryPath) ? [hosLibraryPath] : [];
    case RemotePlatform.ANDROID:
      return androidLibraryPaths;
    case RemotePlatform.IOS:
      return iosLibraryPaths;
    default:
      return [];
  }
}

export const cjmpDoctorWithSpawn = async (): Promise<void> => {
  return new Promise((resolve, reject) => {
    try {
      const keelsCommand = resolveKeelsCommand();
      if (!keelsCommand) {
        resolve();
        return;
      }

      const command = `${keelsCommand.executablePath} 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], { env: keelsCommand.environment })
        : spawn(getDefaultShell(), ['-c', command], { env: keelsCommand.environment });

      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 command 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 execute 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('Failed to find the environment variable for adb');
  }
  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 the system PATH environment variable.\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;
};

export function checkIsValid(val: unknown): boolean {
  if (val === null || val === undefined) {
    return false;
  }
  if (typeof val === 'string' && val === '') {
    return false;
  }
  if (typeof val === 'number' && val === 0) {
    return false;
  }
  if (typeof val === 'boolean') {
    return val;
  }
  return true;
}

export function getOs(): OS {
  switch (os.platform()) {
    case 'win32':
      return 'win';
    default:
      return 'mac';
  }
}

export async function configDefaultEnvToSettings(): Promise<void> {
  const sdkRoot = resolveCjmpSdkRoot({ reportErrors: false });
  if (!sdkRoot) {
    return;
  }
  await resolveAndWriteEnvVars(sdkRoot);
}

function isInsideWorkspace(filePath: string, workspaceFolders: readonly vscode.WorkspaceFolder[]): boolean {
  return workspaceFolders.some((folder) => {
    const relative = path.relative(folder.uri.fsPath, filePath);
    return !relative.startsWith('..') && !path.isAbsolute(relative);
  });
}

export async function waitForCommand(command: string, timeoutMs: number = 30000, intervalMs: number = 100): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const commands = await vscode.commands.getCommands(true);
    if (commands.includes(command)) {
      return true;
    }
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  return false;
}

export async function configureCjpmTomlPath(): Promise<void> {
  let workspaceFolders = vscode.workspace.workspaceFolders;
  if (!workspaceFolders || workspaceFolders.length === 0) {
    return;
  }
  let projectRoot = workspaceFolders[0].uri.fsPath;
  let config = vscode.workspace.getConfiguration();
  const existingPath = config.inspect<string>('Cangjie.Root.Cjpm.Path')?.workspaceValue;
  if (existingPath && fs.existsSync(existingPath) && isInsideWorkspace(existingPath, workspaceFolders)) {
    return;
  }
  let cjpmTomlPath = path.join(projectRoot, 'tests', 'cjpm.toml');
  if (!fs.existsSync(cjpmTomlPath)) {
    cjpmTomlPath = path.join(projectRoot, 'lib', 'cjpm.toml');
  }
  if (!fs.existsSync(cjpmTomlPath)) {
    return;
  }
  try {
    await config.update('Cangjie.Root.Cjpm.Path', cjpmTomlPath, vscode.ConfigurationTarget.Workspace,);
    if (await waitForCommand('cangjie.lsp.reLaunch')) {
      await vscode.commands.executeCommand('cangjie.lsp.reLaunch');
    }
  } catch {
    // Fail silently if the retry mechanism errors; no user notificatio
  }
}

export function getDevecoCangjiePath(): string | undefined {
  const devecoCangjiePath = process.env.DEVECO_CANGJIE_PATH;
  if (!devecoCangjiePath || !fs.existsSync(devecoCangjiePath)) {
    vscode.window.showErrorMessage(
      "DEVECO_CANGJIE_PATH is not set or the path does not exist. Please configure it and restart VS Code."
    );
    return undefined;
  }
  return devecoCangjiePath;
}

export function getOhosVersion(sdkPath?: string): string | undefined {
  const root = sdkPath ?? getDevecoCangjiePath();
  if (!root) {
    return undefined;
  }
  const legacyPkg = path.join(root, "uni-package.json");
  const pkgPath = fs.existsSync(legacyPkg)
    ? legacyPkg
    : path.join(root, "oh-uni-package.json");
  try {
    const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
    return typeof pkg.version === "string" ? pkg.version : undefined;
  } catch {
    return undefined;
  }
}