import {errors} from 'appium/driver';
import {util} from '@appium/support';
import type {AndroidDriver} from '../driver';
import type {IntentOpts} from './types';

const NO_VALUE_ARG_TYPE = 'sn';
const SUPPORTED_EXTRA_TYPES = [
  's',
  NO_VALUE_ARG_TYPE,
  'z',
  'i',
  'l',
  'f',
  'u',
  'cn',
  'ia',
  'ial',
  'la',
  'lal',
  'fa',
  'fal',
  'sa',
  'sal',
] as const;

/**
 * Starts an Android activity.
 *
 * @deprecated Use {@link mobileStartActivity} instead.
 * @param appPackage The package name of the application to start.
 * @param appActivity The activity name to start.
 * @param appWaitPackage The package name to wait for. Defaults to `appPackage` if not provided.
 * @param appWaitActivity The activity name to wait for. Defaults to `appActivity` if not provided.
 * @param intentAction The intent action to use.
 * @param intentCategory The intent category to use.
 * @param intentFlags The intent flags to use.
 * @param optionalIntentArguments Optional intent arguments.
 * @param dontStopAppOnReset If `true`, does not stop the app on reset. If not provided, uses the capability value.
 * @returns Promise that resolves when the activity is started.
 */
export async function startActivity(
  this: AndroidDriver,
  appPackage: string,
  appActivity: string,
  appWaitPackage?: string,
  appWaitActivity?: string,
  intentAction?: string,
  intentCategory?: string,
  intentFlags?: string,
  optionalIntentArguments?: string,
  dontStopAppOnReset?: boolean,
): Promise<void> {
  this.log.debug(`Starting package '${appPackage}' and activity '${appActivity}'`);

  // dontStopAppOnReset is both an argument here, and a desired capability
  // if the argument is set, use it, otherwise use the cap
  if (!util.hasValue(dontStopAppOnReset)) {
    dontStopAppOnReset = !!this.opts.dontStopAppOnReset;
  }

  const args = {
    pkg: appPackage,
    activity: appActivity,
    waitPkg: appWaitPackage || appPackage,
    waitActivity: appWaitActivity || appActivity,
    action: intentAction,
    category: intentCategory,
    flags: intentFlags,
    optionalIntentArguments,
    stopApp: !dontStopAppOnReset,
  };
  this._cachedActivityArgs = this._cachedActivityArgs || {};
  this._cachedActivityArgs[`${args.waitPkg}/${args.waitActivity}`] = args;
  await this.adb.startApp(args);
}

/**
 * Starts an Android activity using the activity manager.
 *
 * @param wait Set it to `true` if you want to block the method call
 * until the activity manager's process returns the control to the system.
 * `false` by default.
 * @param stop Set it to `true` to force stop the target
 * app before starting the activity. `false` by default.
 * @param windowingMode The windowing mode to launch the activity into.
 * Check https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/WindowConfiguration.java
 * for more details on possible windowing modes (constants starting with `WINDOWING_MODE_`).
 * @param activityType The activity type to launch the activity as.
 * Check https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/WindowConfiguration.java
 * for more details on possible activity types (constants starting with `ACTIVITY_TYPE_`).
 * @param display The display identifier to launch the activity into.
 * @param user The user ID for which the activity is started.
 * @param intent The name of the activity intent to start, for example
 * `com.some.package.name/.YourActivitySubClassName`.
 * @param action Action name.
 * @param pkg Package name.
 * @param uri Unified resource identifier.
 * @param mimeType Mime type.
 * @param identifier Optional identifier.
 * @param component Component name.
 * @param categories One or more category names.
 * @param extras Optional intent arguments. Must be represented as array of arrays,
 * where each subarray item contains two or three string items: value type, key name and the value itself.
 * See {@link IntentOpts} for supported value types.
 * @param flags Intent startup-specific flags as a hexadecimal string.
 * @returns Promise that resolves to the command output string.
 */
export async function mobileStartActivity(
  this: AndroidDriver,
  wait?: boolean,
  stop?: boolean,
  windowingMode?: string | number,
  activityType?: string | number,
  display?: number | string,
  user?: string,
  intent?: string,
  action?: string,
  pkg?: string,
  uri?: string,
  mimeType?: string,
  identifier?: string,
  component?: string,
  categories?: string | string[],
  extras?: string[][],
  flags?: string,
): Promise<string> {
  const cmd = ['am', 'start-activity'];
  if (util.hasValue(user)) {
    cmd.push('--user', String(user));
  }
  if (wait) {
    cmd.push('-W');
  }
  if (stop) {
    cmd.push('-S');
  }
  if (util.hasValue(windowingMode)) {
    cmd.push('--windowingMode', String(windowingMode));
  }
  if (util.hasValue(activityType)) {
    cmd.push('--activityType', String(activityType));
  }
  if (util.hasValue(display)) {
    cmd.push('--display', String(display));
  }
  cmd.push(
    ...parseIntentSpec({
      intent,
      action,
      package: pkg,
      uri,
      mimeType,
      identifier,
      component,
      categories,
      extras,
      flags,
    }),
  );
  return await this.adb.shell(cmd);
}

/**
 * Sends a broadcast intent to the Android system.
 *
 * @param receiverPermission Require receiver to hold the given permission.
 * @param allowBackgroundActivityStarts Whether the receiver may start activities even if in the background.
 * @param user The user ID for which the broadcast is sent.
 * The `current` alias assumes the current user ID. `all` by default.
 * @param intent The name of the activity intent to broadcast, for example
 * `com.some.package.name/.YourServiceSubClassName`.
 * @param action Action name.
 * @param pkg Package name.
 * @param uri Unified resource identifier.
 * @param mimeType Mime type.
 * @param identifier Optional identifier.
 * @param component Component name.
 * @param categories One or more category names.
 * @param extras Optional intent arguments. Must be represented as array of arrays,
 * where each subarray item contains two or three string items: value type, key name and the value itself.
 * See {@link IntentOpts} for supported value types.
 * @param flags Intent startup-specific flags as a hexadecimal string.
 * @returns Promise that resolves to the command output string.
 */
export async function mobileBroadcast(
  this: AndroidDriver,
  receiverPermission?: string,
  allowBackgroundActivityStarts?: boolean,
  user?: string | number,
  intent?: string,
  action?: string,
  pkg?: string,
  uri?: string,
  mimeType?: string,
  identifier?: string,
  component?: string,
  categories?: string | string[],
  extras?: string[][],
  flags?: string,
): Promise<string> {
  const cmd = ['am', 'broadcast'];
  if (util.hasValue(user)) {
    cmd.push('--user', String(user));
  }
  if (receiverPermission) {
    cmd.push('--receiver-permission', receiverPermission);
  }
  if (allowBackgroundActivityStarts) {
    cmd.push('--allow-background-activity-starts');
  }
  cmd.push(
    ...parseIntentSpec({
      intent,
      action,
      package: pkg,
      uri,
      mimeType,
      identifier,
      component,
      categories,
      extras,
      flags,
    }),
  );
  return await this.adb.shell(cmd);
}

/**
 * Starts an Android service.
 *
 * @param foreground Set it to `true` if your service must be started as foreground service.
 * This option is ignored if the API level of the device under test is below 26 (Android 8).
 * @param user The user ID for which the service is started.
 * The `current` user id is used by default.
 * @param intent The name of the activity intent to start, for example
 * `com.some.package.name/.YourServiceSubClassName`.
 * @param action Action name.
 * @param pkg Package name.
 * @param uri Unified resource identifier.
 * @param mimeType Mime type.
 * @param identifier Optional identifier.
 * @param component Component name.
 * @param categories One or more category names.
 * @param extras Optional intent arguments. Must be represented as array of arrays,
 * where each subarray item contains two or three string items: value type, key name and the value itself.
 * See {@link IntentOpts} for supported value types.
 * @param flags Intent startup-specific flags as a hexadecimal string.
 * @returns Promise that resolves to the command output string.
 */
export async function mobileStartService(
  this: AndroidDriver,
  foreground?: boolean,
  user?: string,
  intent?: string,
  action?: string,
  pkg?: string,
  uri?: string,
  mimeType?: string,
  identifier?: string,
  component?: string,
  categories?: string | string[],
  extras?: string[][],
  flags?: string,
): Promise<string> {
  const cmd = ['am'];
  cmd.push(foreground ? 'start-foreground-service' : 'start-service');
  if (util.hasValue(user)) {
    cmd.push('--user', String(user));
  }
  cmd.push(
    ...parseIntentSpec({
      intent,
      action,
      package: pkg,
      uri,
      mimeType,
      identifier,
      component,
      categories,
      extras,
      flags,
    }),
  );
  return await this.adb.shell(cmd);
}

/**
 * Stops an Android service.
 *
 * @param user The user ID for which the service is stopped.
 * @param intent The name of the activity intent to stop, for example
 * `com.some.package.name/.YourServiceSubClassName`.
 * @param action Action name.
 * @param pkg Package name.
 * @param uri Unified resource identifier.
 * @param mimeType Mime type.
 * @param identifier Optional identifier.
 * @param component Component name.
 * @param categories One or more category names.
 * @param extras Optional intent arguments. Must be represented as array of arrays,
 * where each subarray item contains two or three string items: value type, key name and the value itself.
 * See {@link IntentOpts} for supported value types.
 * @param flags Intent startup-specific flags as a hexadecimal string.
 * @returns Promise that resolves to the command output string.
 * If the service was already stopped, returns the error message.
 */
export async function mobileStopService(
  this: AndroidDriver,
  user?: string,
  intent?: string,
  action?: string,
  pkg?: string,
  uri?: string,
  mimeType?: string,
  identifier?: string,
  component?: string,
  categories?: string | string[],
  extras?: string[][],
  flags?: string,
): Promise<string> {
  const cmd = ['am', 'stop-service'];
  if (util.hasValue(user)) {
    cmd.push('--user', String(user));
  }
  cmd.push(
    ...parseIntentSpec({
      intent,
      action,
      package: pkg,
      uri,
      mimeType,
      identifier,
      component,
      categories,
      extras,
      flags,
    }),
  );
  try {
    return await this.adb.shell(cmd);
  } catch (e) {
    // https://github.com/appium/appium-uiautomator2-driver/issues/792
    const err = e as {code?: number; stderr?: string};
    if (err.code === 255 && err.stderr?.includes('Service stopped')) {
      return err.stderr;
    }
    throw e;
  }
}

// #region Internal helpers

function parseIntentSpec(opts: IntentOpts = {}): string[] {
  const {intent, action, uri, mimeType, identifier, categories, component, extras, flags} = opts;
  const resultArgs: string[] = [];
  if (intent) {
    resultArgs.push(intent);
  }
  if (action) {
    resultArgs.push('-a', action);
  }
  if (uri) {
    resultArgs.push('-d', uri);
  }
  if (mimeType) {
    resultArgs.push('-t', mimeType);
  }
  if (util.hasValue(identifier)) {
    resultArgs.push('-i', identifier);
  }
  if (categories) {
    if (Array.isArray(categories)) {
      resultArgs.push(...categories.flatMap((cName) => ['-c', cName]));
    } else {
      resultArgs.push('-c', categories);
    }
  }
  if (component) {
    resultArgs.push('-n', component);
  }
  if (opts.package) {
    resultArgs.push('-p', opts.package);
  }
  if (extras) {
    if (!Array.isArray(extras)) {
      throw new errors.InvalidArgumentError(`'extras' must be an array`);
    }
    for (const item of extras) {
      if (!Array.isArray(item)) {
        throw new errors.InvalidArgumentError(`Extra argument '${item}' must be an array`);
      }
      const [type, key, value] = item;
      if (!SUPPORTED_EXTRA_TYPES.includes(type as any)) {
        throw new errors.InvalidArgumentError(
          `Extra argument type '${type}' is not known. ` +
            `Supported intent argument types are: ${SUPPORTED_EXTRA_TYPES.join(', ')}`,
        );
      }
      if (util.isEmpty(key) || (typeof key === 'string' && key.trim() === '')) {
        throw new errors.InvalidArgumentError(
          `Extra argument's key in '${JSON.stringify(item)}' must be a valid string identifier`,
        );
      }
      if (type === NO_VALUE_ARG_TYPE) {
        resultArgs.push(`--e${type}`, key);
      } else if (value === undefined) {
        throw new errors.InvalidArgumentError(
          `Intent argument type '${type}' in '${JSON.stringify(item)}' requires a ` +
            `valid value to be provided`,
        );
      } else {
        resultArgs.push(`--e${type}`, key, value);
      }
    }
  }
  if (flags) {
    resultArgs.push('-f', flags);
  }
  return resultArgs;
}

// #endregion
