import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:dart_framework/utils/string_util.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:xml/xml.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/build/build_flutter.dart';
import 'package:oto/oto/commands/build/fastlane_template.dart';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_runtime.dart';
import 'package:oto/oto/data/command_data.dart';

class BuildiOS extends Command {
  Function? get error => null;

  @override
  Future execute(DataCommand command) async {
    var data = DataBuildiOS.fromJson(getParam(command));
    var xcodeProjectPath =
        data.xcworkspaceFilePath ?? data.xcodeProjectFilePath;
    if (xcodeProjectPath == null) {
      throw Exception(
          '"xcodeProjectFilePath" or "xcworkspaceFilePath" must set at least one.');
    }
    var xcworkspaceFilePath = Command.getPathNormal(xcodeProjectPath);

    String? version = data.version;
    String? buildNumber = data.buildNumber;
    var xcodeproj = data.xcodeProjectFilePath ??
        await getMainXcodeprojPath(data.xcworkspaceFilePath!);

    /************ Auto find build number ************/
    var autoBuildNumber = (data.autoBuildNumber ?? false)
        ? !((version != null && version.isNotEmpty) ||
            (buildNumber != null && buildNumber.isNotEmpty))
        : false;
    if (autoBuildNumber) {
      var appID = await getAppIdentifier(
          data.workspace, xcodeproj, data.scheme, data.setAppID);
      if (appID == null) {
        throw Exception('Can not find App Identifier.');
      }
      if (data.apiKeyPath == null) {
        throw Exception(
            'If autoVersionNumber = true then must set apiKeyPath.');
      }
      var fastFile = await initializeFastlane(
          data.workspace!, data.apiKeyPath!, appID,
          runtime: runtime);
      var fastlaneContent = fastFile.readAsStringSync();

      //Find PREPARE_FOR_SUBMISSION state Version
      String? targetVersion;
      var shell = StringBuffer();
      shell.write('cd ${data.workspace}/fastlane');
      shell.write(' && fastlane appstore_versions');
      var process = await runtime.run(shell);
      var arr = process.stdout.toString().split('\n');
      const result = '[RESULT]: ';
      var needIncrementVersionState = [
        'WAITING_FOR_REVIEW',
        'WAITING_FOR_REVIEW',
        'IN_REVIEW',
        'PENDING_DEVELOPER_RELEASE',
        'PENDING_APPLE_RELEASE',
        'REPLACED_WITH_NEW_VERSION'
      ];
      List<dynamic> jsonArr;
      String? readyForSaleVersion;
      for (var item in arr) {
        if (item.contains(result)) {
          jsonArr =
              jsonDecode(item.substring(item.indexOf(result) + result.length))
                  as List<dynamic>;
          for (var item in jsonArr) {
            if (item['state'] == 'PREPARE_FOR_SUBMISSION') {
              targetVersion = item['version'];
              break;
            }
            if (needIncrementVersionState.contains(item['state'])) {
              targetVersion = incrementVersion(item['version']);
            }
            if (item['state'] == 'READY_FOR_SALE') {
              readyForSaleVersion = item['version'];
            }
          }
          break;
        }
      }
      if (targetVersion == null) {
        if (readyForSaleVersion != null) {
          targetVersion = incrementVersion(readyForSaleVersion);
        } else {
          throw Exception(
              'Can not find target version when state = PREPARE_FOR_SUBMISSION, PENDING_DEVELOPER_RELEASE');
        }
      }
      version = targetVersion;

      //Find Last Build Number
      String? targetBuildNumber;
      fastlaneContent =
          fastlaneContent.replaceFirst('[LATEST_VERSION]', version);
      fastFile.writeAsStringSync(fastlaneContent, flush: true);
      shell = StringBuffer();
      shell.write('cd ${data.workspace}/fastlane');
      shell.write(' && fastlane latest_build_number');
      process = await runtime.run(shell);
      arr = process.stdout.toString().split('\n');
      for (var item in arr) {
        if (item.contains(result)) {
          var json =
              jsonDecode(item.substring(item.indexOf(result) + result.length));
          var key = 'latest_build_number';
          if (json is Map && json.containsKey(key)) {
            targetBuildNumber = json[key].toString();
          }
          break;
        }
      }
      if (targetBuildNumber == null) {
        throw Exception('Can not find target build number.');
      }
      buildNumber =
          incrementVersion(targetBuildNumber, fixedLength: 4, reverse: true);
    }

    String? target = data.target;
    // If version is provided, apply it; otherwise retain the existing value from the project
    if (version != null && version.isNotEmpty) {
      await updateMarketingVersion(
          '${data.workspace}/$xcodeproj/project.pbxproj', version);
    } else {
      target = target ?? await getTargetXcodeproj(data.workspace!, xcodeproj!);
      version = await getSettingXcodeproj(
          data.workspace!, xcodeproj!, target!, 'MARKETING_VERSION');
    }
    // If buildNumber is provided, apply it; otherwise retain the existing value from the project
    if (buildNumber != null && buildNumber.isNotEmpty) {
      await updateBuildNumber(
          '${data.workspace}/$xcodeproj/project.pbxproj', buildNumber);
    } else {
      target = target ?? await getTargetXcodeproj(data.workspace!, xcodeproj!);
      buildNumber = await getSettingXcodeproj(
          data.workspace!, xcodeproj!, target!, 'CURRENT_PROJECT_VERSION');
    }

    var setVersion = data.setVersion != null && data.setVersion!.isNotEmpty
        ? data.setVersion!
        : '<@property.version>';
    await setProperty(setVersion, version);
    var setBuildNumber =
        data.setBuildNumber != null && data.setBuildNumber!.isNotEmpty
            ? data.setBuildNumber!
            : '<@property.buildNumber>';
    await setProperty(setBuildNumber, buildNumber);

    var os = 'iOS';
    if (data.os != null) {
      if (data.os == BuildPlatform.Macos) {
        os = 'macOS';
      }
    }
    var destination = "'generic/platform=$os'";
    var cleanPod = data.cleanPod ?? false;
    var xcodeProjectParentPath = Directory(xcworkspaceFilePath).parent.path;
    var podFile = File('$xcodeProjectParentPath/Podfile');

    var project = '';
    if (data.xcworkspaceFilePath != null) {
      project = '-workspace $xcodeProjectPath';
    } else {
      project = '-project $xcodeProjectPath';
    }

    var configuration = '';
    if (data.configuration != null) {
      configuration = ' -configuration ${data.configuration}';
    }
    var derivedDataPath = '';
    if (data.derivedDataPath != null) {
      derivedDataPath = ' -derivedDataPath ${data.derivedDataPath}';
    }
    var settings = '';
    if (data.settings != null) {
      var map = data.settings!;
      for (var item in map.entries) {
        settings += ' ${item.key}="${item.value}"';
      }
    }

    var shell = StringBuffer(getCDPath(xcodeProjectParentPath));
    if (data.macPassword != null) {
      shell.write(' $and security unlock-keychain -p${data.macPassword}');
    }
    shell.write(' && export LANG=en_US.UTF-8');
    if (podFile.existsSync() && cleanPod) {
      shell.write(' && rm -rf Pods');
      shell.write(' && rm -rf Podfile.lock');
      if (cleanPod) {
        shell.write(' && pod cache clean --all');
        shell.write(' && pod repo update');
        shell.write(' && pod install --repo-update');
      } else {
        shell.write(' && pod install');
      }
    }
    shell.write(
        ' && xcodebuild $project -scheme ${data.scheme} -destination $destination$configuration$derivedDataPath$settings clean build');

    var process = await runtime.start(shell,
        printStderr: false, logHandler: Application.logWithType);
    var exitCode = process.exitCode ?? await process.process.exitCode;
    var success = exitCode == 0;
    if (success) {
      return await completeProcess(process, command);
    } else {
      if (exitCode == 31) {
        data.cleanPod = true;
        command.param = data.toJson();
        return await execute(command);
      } else {
        return await completeProcess(process, command);
      }
    }
  }

  static Future<File> initializeFastlane(
      String workspace, String apiKeyPath, String appID,
      {CommandRuntime? runtime}) async {
    var json = jsonDecode(File(apiKeyPath).readAsStringSync());
    var fastlaneContent = fastlaneTemplate;
    fastlaneContent = fastlaneContent
        .replaceFirst('[APP_IDENTIFIER]', appID)
        .replaceFirst('[API_KEY_ID]', json['key_id'])
        .replaceFirst('[API_ISSUER_ID]', json['issuer_id'])
        .replaceFirst('[AUTH_KEY_CONTENT]',
            json['key'].toString().replaceAll('\n', '\\n'));

    var shell = StringBuffer();
    shell.write('cd $workspace');
    shell.write(' && rm -rf "$workspace/fastlane"');
    shell.write(' && yes | fastlane init');
    await (runtime ?? Command.defaultRuntime).run(shell);

    var fastFile = File('$workspace/fastlane/Fastfile');
    fastFile.writeAsStringSync(fastlaneContent, flush: true);
    return dataFutrue(fastFile);
  }

  Future<String?> getAppIdentifier(String? workspace, String? xcodeproj,
      String scheme, String? setAppID) async {
    String? identifier;
    var shell = StringBuffer();
    shell.write(
        "xcodebuild -project '$workspace/$xcodeproj' -scheme $scheme -destination 'generic/platform=iOS Simulator' -showBuildSettings | grep 'PRODUCT_BUNDLE_IDENTIFIER' | tail -n1 | awk -F ' = ' '{print \$2}'");

    var process = await runtime.start(shell,
        workspace: workspace,
        printStderr: false,
        logHandler: Application.logWithType);
    for (var item in process.stdoutArr) {
      var appID = item.trim().replaceAll(' ', '');
      if (appID.isNotEmpty) {
        setAppID = (setAppID == null) ? '<@property.appIdentifier>' : setAppID;
        await setProperty(setAppID, appID);
        identifier = appID;
        break;
      }
    }
    return dataFutrue(identifier);
  }

  Future<String?> getMainXcodeprojPath(String workspacePath) async {
    final contentsPath = '$workspacePath/contents.xcworkspacedata';
    final file = File(contentsPath);
    if (!await file.exists()) return null;

    final xmlString = await file.readAsString();
    final document = XmlDocument.parse(xmlString);

    final fileRef = document.findAllElements('FileRef').firstWhere(
          (element) =>
              element.getAttribute('location')?.endsWith('.xcodeproj') ?? false,
          orElse: () =>
              XmlElement(XmlName('Dummy')), // sentinel for null-check below
        );

    final location =
        fileRef.name.local == 'Dummy' ? null : fileRef.getAttribute('location');
    if (location != null && location.startsWith('group:')) {
      return location.replaceFirst('group:', '');
    }
    return null;
  }

  Future<void> updateMarketingVersion(
      String pbxprojPath, String version) async {
    await _updatePbxprojSetting(
      pbxprojPath: pbxprojPath,
      settingKey: 'MARKETING_VERSION',
      settingValue: version,
    );
    print('✅ Updated MARKETING_VERSION = $version');
  }

  Future<void> updateBuildNumber(String pbxprojPath, String buildNumber) async {
    await _updatePbxprojSetting(
      pbxprojPath: pbxprojPath,
      settingKey: 'CURRENT_PROJECT_VERSION',
      settingValue: buildNumber,
    );
    print('✅ Updated CURRENT_PROJECT_VERSION = $buildNumber');
  }

  Future<void> _updatePbxprojSetting({
    required String pbxprojPath,
    required String settingKey,
    required String settingValue,
  }) async {
    final file = File(pbxprojPath);
    if (!await file.exists()) throw Exception('File not found: $pbxprojPath');

    final lines = await file.readAsLines();

    final updatedLines = <String>[];
    bool insideBuildSettings = false;
    bool hasTargetSetting = false;

    for (var line in lines) {
      final trimmed = line.trim();

      if (trimmed == 'buildSettings = {') {
        insideBuildSettings = true;
        hasTargetSetting = false;
        updatedLines.add(line);
        continue;
      }

      if (insideBuildSettings && trimmed == '};') {
        // Insert the setting if not already present
        if (!hasTargetSetting) {
          updatedLines.add('        $settingKey = $settingValue;');
        }
        insideBuildSettings = false;
        updatedLines.add(line);
        continue;
      }

      if (insideBuildSettings && trimmed.startsWith('$settingKey =')) {
        line = line.replaceFirst(RegExp('$settingKey = .*?;'),
            '        $settingKey = $settingValue;');
        hasTargetSetting = true;
      }

      updatedLines.add(line);
    }

    await file.writeAsString(updatedLines.join('\n'));
  }

  Future<String?> getSettingXcodeproj(
      String workspace, String xcodeproj, String target, String marker) async {
    String? version;
    var shell = StringBuffer();
    shell.write('cd $workspace');
    shell.write(
        ' && xcodebuild -project $xcodeproj -target $target -showBuildSettings | grep $marker');

    var mark = '$marker = ';
    var process = await runtime.run(shell);
    var arr = process.stdout.toString().split('\n');
    for (var item in arr) {
      if (item.contains(mark)) {
        version = item.replaceAll(mark, '').trim();
        break;
      }
    }
    return dataFutrue(version);
  }

  Future<String?> getTargetXcodeproj(
      String workspace, String xcoodeproj) async {
    String? target;
    var shell = StringBuffer();
    shell.write('cd $workspace');
    shell.write(
        " && xcodebuild -project $xcoodeproj -list | awk '/Targets:/ {getline; print; exit}'");

    var process = await runtime.run(shell);
    var arr = process.stdout.toString().split('\n');
    for (var item in arr) {
      item = item.trim();
      if (item.isNotEmpty) {
        target = item.trim();
        break;
      }
    }
    return dataFutrue(target);
  }
}
