import 'dart:io';

const _fallbackVersion = '0.0.1';
final _versionPattern = RegExp(r'^(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$');

typedef _Release = ({
  String platform,
  String prefix,
  String source,
  _Version version,
});

final class _Version implements Comparable<_Version> {
  const _Version(this.major, this.minor, this.patch);

  factory _Version.parse(String value) {
    final match = _versionPattern.firstMatch(value);
    if (match == null) {
      throw FormatException(
        "Unsupported version '$value'. Expected MAJOR.MINOR.PATCH.",
      );
    }
    return _Version(
      int.parse(match[1]!),
      int.parse(match[2]!),
      int.parse(match[3]!),
    );
  }

  final int major;
  final int minor;
  final int patch;

  _Version get nextPatch => _Version(major, minor, patch + 1);
  int get buildNumber => major * 1000000 + minor * 1000 + patch;

  @override
  int compareTo(_Version other) {
    final majorComparison = major.compareTo(other.major);
    if (majorComparison != 0) return majorComparison;
    final minorComparison = minor.compareTo(other.minor);
    if (minorComparison != 0) return minorComparison;
    return patch.compareTo(other.patch);
  }

  @override
  String toString() => '$major.$minor.$patch';
}

Future<void> main(List<String> arguments) async {
  try {
    await _run(arguments);
  } on FormatException catch (error) {
    stderr.writeln('error: ${error.message}');
    stderr.writeln(_usage);
    exitCode = 2;
  } on StateError catch (error) {
    stderr.writeln('error: ${error.message}');
    exitCode = 1;
  }
}

Future<void> _run(List<String> arguments) async {
  if (arguments.isEmpty || arguments.contains('--help')) {
    stdout.writeln(_usage);
    if (arguments.isEmpty) exitCode = 2;
    return;
  }

  final command = arguments.first;
  final options = _parseOptions(arguments.skip(1).toList());
  final projectRoot = options.projectRoot ?? _defaultProjectRoot;

  switch (command) {
    case 'current':
      _expectOnePlatform(options);
      stdout.writeln(
        await _currentVersion(options.positional.single, projectRoot),
      );
    case 'next':
      _expectOnePlatform(options);
      final release = await _nextRelease(
        options.positional.single,
        projectRoot,
      );
      stdout.writeln(
        '${release.platform} ${release.version} ${release.source}',
      );
    case 'build-number':
      if (options.positional.length != 1 || options.hasTagOptions) {
        throw const FormatException('build-number requires one version.');
      }
      stdout.writeln(_Version.parse(options.positional.single).buildNumber);
    case 'tag':
      if (options.positional.length != 1) {
        throw const FormatException('tag requires one platform.');
      }
      await _tag(options.positional.single, projectRoot, options);
    default:
      throw FormatException("Unknown command '$command'.");
  }
}

void _expectOnePlatform(_Options options) {
  if (options.positional.length != 1 || options.hasTagOptions) {
    throw const FormatException('current/next requires one platform.');
  }
}

({String platform, String prefix}) _resolvePlatform(String value) {
  return switch (value.trim().toLowerCase()) {
    'windows' ||
    'pc' ||
    'macos' ||
    'darwin' => (platform: 'windows', prefix: 'v'),
    'android' => (platform: 'android', prefix: 'android-v'),
    _ => throw FormatException(
      "Unsupported platform '$value'. Expected windows, pc, macos, or android.",
    ),
  };
}

Future<_Version> _currentVersion(String platform, String projectRoot) async {
  final pubspec = _pubspecVersion(projectRoot);
  final latest = await _latestTag(platform, projectRoot);
  return latest == null || latest.version.compareTo(pubspec) < 0
      ? pubspec
      : latest.version;
}

Future<_Release> _nextRelease(String platform, String projectRoot) async {
  final resolved = _resolvePlatform(platform);
  final pubspec = _pubspecVersion(projectRoot);
  final latest = await _latestTag(resolved.platform, projectRoot);
  if (latest == null || latest.version.compareTo(pubspec) < 0) {
    return (
      platform: resolved.platform,
      prefix: resolved.prefix,
      source: 'pubspec:$pubspec',
      version: pubspec,
    );
  }
  return (
    platform: resolved.platform,
    prefix: resolved.prefix,
    source: latest.tag,
    version: latest.version.nextPatch,
  );
}

_Version _pubspecVersion(String projectRoot) {
  final pubspec = File('$projectRoot${Platform.pathSeparator}pubspec.yaml');
  if (!pubspec.existsSync()) return _Version.parse(_fallbackVersion);
  final match = RegExp(
    r'^version:\s*(\d+\.\d+\.\d+(?:\+[0-9A-Za-z.-]+)?)\s*$',
    multiLine: true,
  ).firstMatch(pubspec.readAsStringSync());
  return _Version.parse(match?[1] ?? _fallbackVersion);
}

Future<({String tag, _Version version})?> _latestTag(
  String platform,
  String projectRoot,
) async {
  final resolved = _resolvePlatform(platform);
  final result = await Process.run('git', [
    'tag',
    '--list',
    '${resolved.prefix}*',
  ], workingDirectory: projectRoot);
  if (result.exitCode != 0) return null;

  final pattern = RegExp(
    '^${RegExp.escape(resolved.prefix)}'
    r'(\d+\.\d+\.\d+(?:\+[0-9A-Za-z.-]+)?)$',
  );
  ({String tag, _Version version})? latest;
  for (final tag in '${result.stdout}'.split(RegExp(r'\r?\n'))) {
    final match = pattern.firstMatch(tag.trim());
    if (match == null) continue;
    final candidate = (tag: tag.trim(), version: _Version.parse(match[1]!));
    if (latest == null || candidate.version.compareTo(latest.version) > 0) {
      latest = candidate;
    }
  }
  return latest;
}

Future<void> _tag(
  String requestedPlatform,
  String projectRoot,
  _Options options,
) async {
  final platforms = requestedPlatform.toLowerCase() == 'both'
      ? const ['windows', 'android']
      : [_resolvePlatform(requestedPlatform).platform];

  for (final platform in platforms) {
    final release = await _nextRelease(platform, projectRoot);
    final tag = '${release.prefix}${release.version}';
    final existing = await _git(['tag', '--list', tag], projectRoot);
    if ('${existing.stdout}'.trim().isNotEmpty) {
      throw StateError("Tag '$tag' already exists. Aborting.");
    }

    stdout.writeln('[${release.platform}] ${release.source} -> $tag');
    if (options.dryRun) {
      stdout.writeln('  DryRun: skip create/push.');
      continue;
    }

    await _git(['tag', tag], projectRoot);
    if (options.noPush) {
      stdout.writeln('  Created local tag (--no-push).');
      continue;
    }
    await _git(['push', options.remote, tag], projectRoot);
    stdout.writeln('  Pushed to ${options.remote}.');
  }
}

Future<ProcessResult> _git(List<String> arguments, String projectRoot) async {
  final result = await Process.run(
    'git',
    arguments,
    workingDirectory: projectRoot,
  );
  if (result.exitCode != 0) {
    throw StateError(
      '${result.stderr}'.trim().isEmpty
          ? 'git ${arguments.join(' ')} failed with exit ${result.exitCode}.'
          : '${result.stderr}'.trim(),
    );
  }
  return result;
}

_Options _parseOptions(List<String> arguments) {
  final positional = <String>[];
  String? projectRoot;
  var dryRun = false;
  var noPush = false;
  var remote = 'origin';

  for (var index = 0; index < arguments.length; index++) {
    final argument = arguments[index];
    switch (argument) {
      case '--dry-run':
        dryRun = true;
      case '--no-push':
        noPush = true;
      case '--project-root' || '--remote':
        if (++index >= arguments.length) {
          throw FormatException('$argument requires a value.');
        }
        if (argument == '--project-root') {
          projectRoot = arguments[index];
        } else {
          remote = arguments[index];
        }
      default:
        if (argument.startsWith('--project-root=')) {
          projectRoot = argument.substring('--project-root='.length);
        } else if (argument.startsWith('--remote=')) {
          remote = argument.substring('--remote='.length);
        } else if (argument.startsWith('-')) {
          throw FormatException("Unknown option '$argument'.");
        } else {
          positional.add(argument);
        }
    }
  }
  if (remote.isEmpty) throw const FormatException('remote cannot be empty.');
  return _Options(positional, projectRoot, dryRun, noPush, remote);
}

final class _Options {
  const _Options(
    this.positional,
    this.projectRoot,
    this.dryRun,
    this.noPush,
    this.remote,
  );

  final List<String> positional;
  final String? projectRoot;
  final bool dryRun;
  final bool noPush;
  final String remote;

  bool get hasTagOptions => dryRun || noPush || remote != 'origin';
}

String get _defaultProjectRoot =>
    File.fromUri(Platform.script).parent.parent.path;

const _usage = '''Usage:
  dart scripts/version.dart current <pc|windows|macos|android>
  dart scripts/version.dart next <pc|windows|macos|android>
  dart scripts/version.dart build-number <MAJOR.MINOR.PATCH>
  dart scripts/version.dart tag <pc|windows|android|both> [--dry-run] [--no-push] [--remote=<name>]''';
