import type { AdbExecutor } from "../../../utils/android-cmdline-tools/interfaces/AdbExecutor";
import { readAndroidDeviceApiLevel } from "../../../utils/android-cmdline-tools/readAndroidDeviceApiLevel";
import { logger } from "../../../utils/logger";
import { AndroidCtrlProxyClient } from "../../observe/android/AndroidCtrlProxyClient";
import type { SettingsNamespace } from "../../observe/android";
import type {
  BootedDevice,
  GetCalendarSystemResult,
  LocalizationSettingsResult,
  Set24HourFormatResult,
  SetCalendarSystemResult,
  SetLocaleResult,
  SetTextDirectionResult,
  SetTimeZoneResult,
} from "../../../models";
import type {
  BroadcastOptions,
  SystemConfigurationAdapter,
} from "../../../utils/interfaces/SystemConfigurationAdapter";
import {
  extractCalendarFromLocale,
  normalizeSettingValue,
  normalizeTimeFormat,
  parseBooleanSetting,
  parseLocaleList,
} from "./parsing";

type TextDirectionSettingKey = "debug.force_rtl" | "force_rtl";

type CommandAttempt = {
  method: string;
  command: string;
};

/**
 * Android implementation of {@link SystemConfigurationAdapter}. Uses
 * ADB shell commands (with an accessibility-service fast path for
 * `settings get`/`settings put`) and `getprop`/`setprop` to read and
 * write locale, time zone, RTL, 24-hour format, and calendar
 * settings.
 */
export class AndroidSystemConfigurationAdapter implements SystemConfigurationAdapter {
  readonly defaultCalendarSystem = "gregory";

  constructor(
    private readonly device: BootedDevice,
    private readonly adb: AdbExecutor
  ) {}

  async setLocale(languageTag: string, options: BroadcastOptions): Promise<SetLocaleResult> {
    const previousLanguageTag = await this.getCurrentLocaleTag();
    const apiLevel = await readAndroidDeviceApiLevel(this.adb);

    const commandAttempts: CommandAttempt[] = [];
    const cmdLocaleAttempt = {
      method: "cmd locale set-locales",
      command: `shell cmd locale set-locales ${languageTag}`
    };
    const settingsAttempt = {
      method: "settings put system user_locale",
      command: `shell settings put system user_locale ${languageTag}`
    };

    if (apiLevel === null || apiLevel >= 31) {
      commandAttempts.push(cmdLocaleAttempt, settingsAttempt);
    } else {
      commandAttempts.push(settingsAttempt, cmdLocaleAttempt);
    }

    let appliedMethod: string | null = null;
    let lastError: string | null = null;

    for (const attempt of commandAttempts) {
      try {
        await this.runShellCommand(attempt.command);
        appliedMethod = attempt.method;
        break;
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        lastError = errorMessage;
        logger.warn(`[SystemConfigurationManager] Failed locale command (${attempt.method}): ${errorMessage}`);
      }
    }

    if (!appliedMethod) {
      return {
        success: false,
        languageTag,
        previousLanguageTag,
        error: `Failed to set locale${lastError ? `: ${lastError}` : ""}`
      };
    }

    const broadcasted = options.broadcast === false
      ? false
      : await this.broadcastLocaleChange();

    return {
      success: true,
      languageTag,
      previousLanguageTag,
      method: appliedMethod,
      broadcasted
    };
  }

  async setTimeZone(zoneId: string): Promise<SetTimeZoneResult> {
    const previousZoneId = await this.readSetting("shell getprop persist.sys.timezone");

    try {
      await this.adb.executeCommand(`shell setprop persist.sys.timezone ${zoneId}`);
      return {
        success: true,
        zoneId,
        previousZoneId
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        success: false,
        zoneId,
        previousZoneId,
        error: `Failed to set time zone: ${errorMessage}`
      };
    }
  }

  async setTextDirection(rtl: boolean, options: BroadcastOptions): Promise<SetTextDirectionResult> {
    const debugForceRtl = await this.readSetting("shell settings get global debug.force_rtl");
    const forceRtl = await this.readSetting("shell settings get global force_rtl");
    const previousRtl = parseBooleanSetting(debugForceRtl ?? forceRtl);

    const targetKeys: TextDirectionSettingKey[] = [];
    const shouldSetDebug = debugForceRtl !== null || forceRtl === null;
    const shouldSetForce = forceRtl !== null;

    if (shouldSetDebug) {
      targetKeys.push("debug.force_rtl");
    }
    if (shouldSetForce) {
      targetKeys.push("force_rtl");
    }
    if (targetKeys.length === 0) {
      targetKeys.push("debug.force_rtl");
    }

    const appliedSettings: TextDirectionSettingKey[] = [];
    const value = rtl ? 1 : 0;

    for (const key of targetKeys) {
      try {
        await this.runShellCommand(`shell settings put global ${key} ${value}`);
        appliedSettings.push(key);
      } catch (error) {
        logger.warn(`[SystemConfigurationManager] Failed to set ${key}: ${error}`);
      }
    }

    if (appliedSettings.length === 0) {
      return {
        success: false,
        rtl,
        previousRtl,
        error: "Failed to update RTL settings"
      };
    }

    const broadcasted = options.broadcast === false
      ? false
      : await this.broadcastLocaleChange();

    return {
      success: true,
      rtl,
      previousRtl,
      settings: appliedSettings,
      broadcasted
    };
  }

  async set24HourFormat(enabled: boolean): Promise<Set24HourFormatResult> {
    const previousFormat = await this.readSetting("shell settings get system time_12_24");
    const value = enabled ? "24" : "12";

    try {
      await this.runShellCommand(`shell settings put system time_12_24 ${value}`);
      return {
        success: true,
        enabled,
        previousFormat: normalizeTimeFormat(previousFormat)
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        success: false,
        enabled,
        previousFormat: normalizeTimeFormat(previousFormat),
        error: `Failed to set 24-hour format: ${errorMessage}`
      };
    }
  }

  async setCalendarSystem(calendarSystem: string): Promise<SetCalendarSystemResult> {
    const previous = await this.getCalendarSystem();
    const previousCalendarSystem = previous.calendarSystem ?? null;

    try {
      await this.runShellCommand(`shell settings put system calendar_type ${calendarSystem}`);
      const readBack = await this.readSetting("shell settings get system calendar_type");
      if (!readBack || readBack !== calendarSystem) {
        return {
          success: false,
          calendarSystem: readBack ?? calendarSystem,
          previousCalendarSystem,
          error: `Read-back verification failed: expected "${calendarSystem}" but got "${readBack ?? "null"}"`
        };
      }
      return {
        success: true,
        calendarSystem: readBack,
        previousCalendarSystem
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        success: false,
        calendarSystem,
        previousCalendarSystem,
        error: `Failed to set calendar system: ${errorMessage}`
      };
    }
  }

  async getCalendarSystem(): Promise<GetCalendarSystemResult> {
    const calendarType = await this.readSetting("shell settings get system calendar_type");
    if (calendarType) {
      return {
        success: true,
        calendarSystem: calendarType,
        source: "settings.calendar_type"
      };
    }

    const locale = await this.getCurrentLocaleTag();
    if (locale) {
      const calendarFromLocale = extractCalendarFromLocale(locale);
      if (calendarFromLocale) {
        return {
          success: true,
          calendarSystem: calendarFromLocale,
          locale,
          source: "locale"
        };
      }
    }

    return {
      success: true,
      calendarSystem: this.defaultCalendarSystem,
      locale: locale ?? null,
      source: "default"
    };
  }

  async getLocalizationSettings(): Promise<LocalizationSettingsResult> {
    const locale = await this.getCurrentLocaleTag();
    const timeZone = await this.readSetting("shell getprop persist.sys.timezone");
    const timeFormat = normalizeTimeFormat(
      await this.readSetting("shell settings get system time_12_24")
    );
    const debugForceRtl = await this.readSetting("shell settings get global debug.force_rtl");
    const forceRtl = await this.readSetting("shell settings get global force_rtl");
    const rtlSetting = parseBooleanSetting(debugForceRtl) ?? parseBooleanSetting(forceRtl);
    const textDirection = rtlSetting === null ? null : (rtlSetting ? "rtl" : "ltr");
    const calendarResult = await this.getCalendarSystem();

    return {
      success: calendarResult.success,
      locale,
      timeZone,
      textDirection,
      timeFormat,
      calendarSystem: calendarResult.calendarSystem ?? null,
      error: calendarResult.error
    };
  }

  async broadcastLocaleChange(): Promise<boolean> {
    try {
      await this.adb.executeCommand("shell am broadcast -a android.intent.action.LOCALE_CHANGED");
      return true;
    } catch (error) {
      logger.warn(`[SystemConfigurationManager] Failed to broadcast localization change: ${error}`);
      return false;
    }
  }

  // Tries the accessibility-service fast path for `settings get …`
  // first, then falls back to ADB. Any other shell command is passed
  // straight through to `adb.executeCommand`.
  private async readSetting(command: string): Promise<string | null> {
    const match = command.match(/^shell\s+settings\s+get\s+(system|secure|global)\s+(\S+)\s*$/);
    if (match) {
      const namespace = match[1] as SettingsNamespace;
      const key = match[2];
      try {
        const a11y = AndroidCtrlProxyClient.getInstance(this.device);
        const a11yResult = await a11y.requestSettingsGet(namespace, key);
        if (a11yResult.success) {
          return normalizeSettingValue(a11yResult.found ? (a11yResult.value ?? null) : null);
        }
      } catch (error) {
        logger.debug(`[SystemConfigurationManager] a11y settings get failed for ${namespace}/${key}: ${error}`);
      }
    }
    try {
      const result = await this.adb.executeCommand(command, undefined, undefined, true);
      return normalizeSettingValue(result.stdout);
    } catch (error) {
      logger.warn(`[SystemConfigurationManager] Failed to read setting (${command}): ${error}`);
      return null;
    }
  }

  // Intercepts `shell settings put <ns> <key> <value>` and routes
  // through the accessibility service first, falling back to ADB. Any
  // other shell command is passed through to `adb.executeCommand`.
  private async runShellCommand(command: string): Promise<void> {
    const putMatch = command.match(/^shell\s+settings\s+put\s+(system|secure|global)\s+(\S+)\s+(.+)$/);
    if (putMatch) {
      const namespace = putMatch[1] as SettingsNamespace;
      const key = putMatch[2];
      const value = putMatch[3].trim();
      try {
        const a11y = AndroidCtrlProxyClient.getInstance(this.device);
        const a11yResult = await a11y.requestSettingsPut(namespace, key, value, "string");
        if (a11yResult.success) {
          return;
        }
      } catch (error) {
        logger.debug(`[SystemConfigurationManager] a11y settings put failed for ${namespace}/${key}: ${error}`);
      }
    }
    await this.adb.executeCommand(command);
  }

  private async getCurrentLocaleTag(): Promise<string | null> {
    const systemLocales = await this.readSetting("shell settings get system system_locales");
    const parsedSystemLocale = parseLocaleList(systemLocales);
    if (parsedSystemLocale) {
      return parsedSystemLocale;
    }

    const userLocale = await this.readSetting("shell settings get system user_locale");
    if (userLocale) {
      return userLocale;
    }

    const persistedLocale = await this.readSetting("shell getprop persist.sys.locale");
    if (persistedLocale) {
      return persistedLocale;
    }

    const language = await this.readSetting("shell getprop persist.sys.language");
    if (!language) {
      return null;
    }

    const country = await this.readSetting("shell getprop persist.sys.country");
    if (country) {
      return `${language}-${country}`;
    }

    return language;
  }
}
