# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import hashlib
import json
import logging
import pathlib
import os
import subprocess
import sys
import zipfile
import re

_SRC_PATH = pathlib.Path(__file__).resolve().parents[2]

sys.path.insert(1, str(_SRC_PATH / 'third_party/depot_tools'))
import gclient_eval


_FETCH_ALL_PATH = _SRC_PATH / 'third_party/android_deps/fetch_all.py'
_HASH_LENGTH = 15
_SKIP_FILES = ('OWNERS', 'cipd.yaml')

_DEFAULT_GENERATED_DISCLAIMER = '''\
// **IMPORTANT**: build.gradle is generated and any changes would be overridden
//                by the autoroller. Please update build.gradle.template
//                instead.
'''

ANDROIDX_CIPD_PACKAGE = 'chromium/third_party/androidx'


def _get_current_cipd_instance():
  with open(os.path.join(_SRC_PATH, 'DEPS'), 'rt') as f:
    gclient_dict = gclient_eval.Exec(f.read())
    return gclient_eval.GetCIPD(gclient_dict, 'src/third_party/androidx/cipd',
                                ANDROIDX_CIPD_PACKAGE)


def _query_cipd_tags(version):
  cipd_output = subprocess.check_output(
      ['cipd', 'describe', ANDROIDX_CIPD_PACKAGE, '-version', version],
      encoding='utf-8')
  # Output looks like:
  # Package:       chromium/third_party/androidx
  # Instance ID:   gUjEawxv5mQO8yfbuC8W-rx4V3zYE-4LTWggXpZHI4sC
  # Registered by: user:chromium-cipd-builder@chops-service-accounts.iam.gserviceaccount.com
  # Registered at: 2025-01-06 17:54:48.034135 +0000 UTC
  # Refs:
  #   latest
  # Tags:
  #   details0:version-cr-012873390
  #   version:cr-012873390
  lines = cipd_output.split('\n')
  tags = {}
  parsing_tags = False
  for line in lines:
    if not line.strip():
      continue
    if line.startswith('Tags:'):
      parsing_tags = True
      continue
    if parsing_tags:
      tag, value = line.strip().split(':', 1)
      tags[tag] = value
  return tags


def get_current_androidx_version():
  cipd_instance = _get_current_cipd_instance()
  cipd_tags = _query_cipd_tags(cipd_instance)
  version_string = cipd_tags['version']
  version = version_string[len('cr-0'):]
  logging.info('Resolved current androidx version to %s', version)
  return version


def make_androidx_maven_url(version):
  return ('https://androidx.dev/snapshots/builds/' + version +
          '/artifacts/repository')


def generate_version_map_str(bom_path, with_hash=False):
  """Generate groovy code to fill the versionCache map.

    Args:
      bom_path: Path to bill_of_materials.json to parse.
      with_hash: Whether to also return a hash of all the packages in the BoM.
    """
  bom = []
  version_map_lines = []
  bom_hash = hashlib.sha256()
  with open(bom_path) as f:
    bom = json.load(f)
  bom.sort(key=lambda x: (x['group'], x['name']))
  for dep in bom:
    group = dep['group']
    name = dep['name']
    version = dep['version']
    bom_hash.update(f'${group}:${name}:${version}'.encode())
    map_line = f"versionCache['{group}:{name}'] = '{version}'"
    version_map_lines.append(map_line)
  version_map_str = '\n'.join(sorted(version_map_lines))
  version_hash = bom_hash.hexdigest()[:_HASH_LENGTH]
  if with_hash:
    return version_map_str, version_hash
  return version_map_str


def hash_files(files):
  """Returns the sha256 hash of |files| contents."""
  content_hash = hashlib.sha256()
  for filepath in files:
    with open(filepath, 'rb') as f:
      content_hash.update(f.read())
  return content_hash.hexdigest()[:_HASH_LENGTH]


def fill_template(template_path, output_path, **kwargs):
  """Fills in a template.

    Args:
      template_path: Path to <file>.template.
      output_path: Path to <file>.
      **kwargs: each kwarg should be a string to replace in the template.
    """
  content = pathlib.Path(template_path).read_text()
  for key, value in kwargs.items():
    replace_string = '{{' + key + '}}'
    if not replace_string in content:
      raise Exception(f'Replace text {replace_string} '
                      f'not found in {template_path}')
    try:
      content = content.replace(replace_string, value)
    except Exception as e:
      raise e from Exception(
          f'Failed to replace {repr(replace_string)} with {repr(value)}')

  content = content.replace(r'{{generated_disclaimer}}',
                            _DEFAULT_GENERATED_DISCLAIMER)

  unreplaced_variable_re = re.compile(r'\{\{(.+)\}\}')
  if matches := unreplaced_variable_re.findall(content):
    unreplaced_variables = ', '.join(repr(match) for match in matches)
    raise Exception('Found unreplaced variables '
                    f'[{unreplaced_variables}] in {template_path}')

  pathlib.Path(output_path).write_text(content)


def write_cipd_yaml(package_root,
                    package_name,
                    version,
                    output_path,
                    experimental=False):
  """Writes cipd.yaml file at the passed-in path."""

  root_libs_dir = package_root / 'libs'
  lib_dirs = os.listdir(root_libs_dir)
  if not lib_dirs:
    raise Exception('No generated libraries in {}'.format(root_libs_dir))

  data_files = [
      'BUILD.gn',
      'VERSION.txt',
      'bill_of_materials.json',
      'additional_readme_paths.json',
      'build.gradle',
      'to_commit.zip',
  ]
  for lib_dir in lib_dirs:
    abs_lib_dir: pathlib.Path = root_libs_dir / lib_dir
    if not abs_lib_dir.is_dir():
      continue

    for lib_file in abs_lib_dir.iterdir():
      if lib_file.name in _SKIP_FILES:
        continue
      data_files.append((abs_lib_dir / lib_file).relative_to(package_root))

  if experimental:
    package_name = (f'experimental/google.com/{os.getlogin()}/{package_name}')
  contents = [
      '# Copyright 2025 The Chromium Authors',
      '# Use of this source code is governed by a BSD-style license that can be',
      '# found in the LICENSE file.',
      f'# version: {version}',
      f'package: {package_name}',
      f'description: CIPD package for {package_name}',
      'data:',
  ]
  contents.extend(f'- file: {str(f)}' for f in data_files)

  with open(output_path, 'w') as out:
    out.write('\n'.join(contents))


def create_to_commit_zip(output_path, package_root, dirnames,
                         absolute_file_map):
  """Generates a to_commit.zip from useful text files inside |package_root|.

    Args:
      output_path: where to output the zipfile.
      package_root: path to gradle/cipd package.
      dirnames: list of subdirs under |package_root| to walk.
      absolute_file_map: List of files to be stored under the absolute prefix
        CHROMIUM_SRC/.
  """
  to_commit_paths = []
  for directory in dirnames:
    for root, _, files in os.walk(package_root / directory):
      for filename in files:
        # Avoid committing actual artifacts.
        if filename.endswith(('.aar', '.jar')):
          continue
        # TODO(mheikal): stop outputting these from gradle since they are not
        # useful.
        if filename in _SKIP_FILES:
          continue
        file_path = pathlib.Path(root) / filename
        file_path_in_zip = file_path.relative_to(package_root)
        to_commit_paths.append((file_path, file_path_in_zip))

  for filename, path_in_repo in absolute_file_map.items():
    file_path = package_root / filename
    path_in_zip = f'CHROMIUM_SRC/{path_in_repo}'
    to_commit_paths.append((file_path, path_in_zip))

  with zipfile.ZipFile(output_path, 'w') as zip_file:
    for filename, arcname in to_commit_paths:
      zip_file.write(filename, arcname=arcname)


def run_fetch_all(android_deps_dir,
                  extra_args,
                  verbose_count=0,
                  output_subdir=None):
  fetch_all_cmd = [_FETCH_ALL_PATH, '--android-deps-dir', android_deps_dir]
  fetch_all_cmd += ['-v'] * verbose_count
  if output_subdir:
    fetch_all_cmd += ['--output-subdir', output_subdir]

  # Filter out -- from the args to pass to fetch_all.py.
  fetch_all_cmd += [a for a in extra_args if a != '--']

  subprocess.run(fetch_all_cmd, check=True)
