// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

/// Runs the local SPDX and REUSE metadata validation tool.
library;

import 'dart:io';

/// SPDX expression used by package and app source metadata.
///
/// The top-level `LICENSE.md` contains the GNU Affero General Public License
/// version 3 text, so the repository uses the corresponding SPDX identifier.
const _licenseIdentifier = 'AGPL-3.0-only';

/// Repository-local copyright text used in SPDX headers.
///
/// The project uses a contributor-oriented copyright holder because individual
/// source files do not currently carry more specific ownership notices.
const _copyrightText = '2026 FocusFlow contributors';

/// Package directories that must include a pointer to the root license file.
///
/// Each Dart package is publishable as its own unit even though publishing is
/// disabled, so every package folder carries a small `LICENSE.md` reference.
const _packageDirectories = <String>[
  'packages/scheduler_backup',
  'packages/scheduler_core',
  'packages/scheduler_export',
  'packages/scheduler_export_json',
  'packages/scheduler_integration_tests',
  'packages/scheduler_notifications',
  'packages/scheduler_notifications_desktop',
  'packages/scheduler_persistence',
  'packages/scheduler_persistence_memory',
  'packages/scheduler_persistence_sqlite',
];

/// App directories that must include a pointer to the root license file.
///
/// The Flutter app is outside the root Dart workspace, but it still ships with
/// repository code and therefore follows the same licensing metadata rules.
const _appDirectories = <String>['apps/focus_flow_flutter'];

/// Validates SPDX headers, REUSE metadata, and package/app license pointers.
void main(List<String> arguments) {
  final failures = <String>[
    ..._reuseTomlFailures(),
    ..._licensePointerFailures(),
    ..._spdxHeaderFailures(),
  ];

  if (failures.isEmpty) {
    stdout.writeln('SPDX and REUSE metadata checks passed.');
    return;
  }

  stderr.writeln('SPDX and REUSE metadata checks failed:');
  for (final failure in failures) {
    stderr.writeln('- $failure');
  }
  exitCode = 1;
}

/// Checks that `REUSE.toml` exists and covers package/app trees.
///
/// This does not replace the upstream REUSE tool, but it keeps the repository's
/// expected local metadata shape from drifting when contributors add files.
List<String> _reuseTomlFailures() {
  final file = File('REUSE.toml');
  if (!file.existsSync()) {
    return <String>['REUSE.toml is missing.'];
  }

  final contents = file.readAsStringSync();
  final failures = <String>[];
  for (final required in <String>[
    'packages/**',
    'apps/focus_flow_flutter/**',
    'SPDX-License-Identifier = "$_licenseIdentifier"',
    'SPDX-FileCopyrightText = "$_copyrightText"',
  ]) {
    if (!contents.contains(required)) {
      failures.add('REUSE.toml does not contain `$required`.');
    }
  }
  return failures;
}

/// Checks package and app `LICENSE.md` pointer files.
///
/// Pointer files keep each package understandable when opened in isolation while
/// still making the root `LICENSE.md` the single license text source.
List<String> _licensePointerFailures() {
  final failures = <String>[];
  for (final directory in <String>[
    ..._packageDirectories,
    ..._appDirectories
  ]) {
    final file = File('$directory/LICENSE.md');
    if (!file.existsSync()) {
      failures.add('$directory/LICENSE.md is missing.');
      continue;
    }
    final contents = file.readAsStringSync();
    if (!contents.contains('../../LICENSE.md')) {
      failures
          .add('$directory/LICENSE.md does not reference ../../LICENSE.md.');
    }
    if (!contents.contains('SPDX-License-Identifier: $_licenseIdentifier')) {
      failures.add('$directory/LICENSE.md is missing its SPDX license header.');
    }
  }
  return failures;
}

/// Checks safe text files for inline SPDX headers.
///
/// Binary files, JSON resources, generated platform project files, and XML/plist
/// files are covered by `REUSE.toml` annotations because adding comments to
/// those formats can either be invalid or be overwritten by platform tooling.
List<String> _spdxHeaderFailures() {
  final failures = <String>[];
  for (final path in _trackedFiles()) {
    if (!_requiresInlineSpdxHeader(path)) {
      continue;
    }
    final contents = File(path).readAsStringSync();
    if (!contents.contains('SPDX-License-Identifier: $_licenseIdentifier')) {
      failures.add('$path is missing SPDX-License-Identifier.');
    }
    if (!contents.contains('SPDX-FileCopyrightText: $_copyrightText')) {
      failures.add('$path is missing SPDX-FileCopyrightText.');
    }
  }
  return failures;
}

/// Returns tracked repository files in stable path order.
///
/// Git is preferred so ignored build output and local editor files do not affect
/// the metadata check. Container build contexts may omit `.git`, so the checker
/// falls back to a conservative tree walk over the compliance scope.
List<String> _trackedFiles() {
  final result = Process.runSync(
    'git',
    <String>['ls-files'],
    runInShell: Platform.isWindows,
  );
  if (result.exitCode == 0) {
    return (result.stdout as String)
        .split('\n')
        .where((path) => path.trim().isNotEmpty)
        .toList(growable: false)
      ..sort();
  }

  return _walkComplianceFiles();
}

/// Walks source-controlled compliance paths when Git metadata is unavailable.
///
/// This path is used by Earthly and other copied build contexts. Generated
/// output and tool caches are skipped explicitly so stale local artifacts do not
/// create false failures.
List<String> _walkComplianceFiles() {
  final files = <String>[];
  for (final entity in Directory.current.listSync(recursive: true)) {
    if (entity is! File) {
      continue;
    }
    final path = entity.path.replaceAll('\\', '/').replaceFirst('./', '');
    if (_isIgnoredGeneratedPath(path) || !_isComplianceScope(path)) {
      continue;
    }
    files.add(path);
  }
  return files..sort();
}

/// Reports whether [path] belongs to generated output or local tooling caches.
///
/// These paths are intentionally ignored by the fallback scanner because they
/// are not source files and are normally excluded by Git.
bool _isIgnoredGeneratedPath(String path) {
  return path.startsWith('.dart_tool/') ||
      path.startsWith('.git/') ||
      path.startsWith('build/') ||
      path.startsWith('builds/') ||
      path.startsWith('coverage/') ||
      path.startsWith('doc/api/') ||
      path.contains('/.dart_tool/') ||
      path.contains('/build/') ||
      path.contains('/.plugin_symlinks/');
}

/// Reports whether [path] should carry an inline SPDX header.
///
/// The allow-list is intentionally conservative and includes only formats where
/// adding a leading comment is safe and unlikely to break generated tooling.
bool _requiresInlineSpdxHeader(String path) {
  if (!_isComplianceScope(path)) {
    return false;
  }
  if (path.endsWith('.g.dart') || path.endsWith('LICENSE.md')) {
    return false;
  }
  return path == '.earthlyignore' ||
      path == '.gitignore' ||
      path == 'Earthfile' ||
      path.endsWith('.dart') ||
      path.endsWith('.yaml') ||
      path.endsWith('.yml') ||
      path.endsWith('.sh') ||
      path.endsWith('.md');
}

/// Reports whether [path] belongs to the checked compliance surface.
///
/// The package and app folders are the main requested scope; root scripts and
/// tool files are included because they enforce the same standards.
bool _isComplianceScope(String path) {
  return path.startsWith('packages/') ||
      path.startsWith('apps/focus_flow_flutter/') ||
      path.startsWith('scripts/') ||
      path.startsWith('tool/') ||
      path == '.earthlyignore' ||
      path == '.gitignore' ||
      path == 'AGENTS.md' ||
      path == 'Earthfile' ||
      path == 'Focus Flow Contributors.md' ||
      path == 'README.md' ||
      path == 'pubspec.yaml' ||
      path == 'analysis_options.yaml' ||
      path == '.github/workflows/ci.yml';
}
