import { ExternalHostError } from '../../../types/errors/external-host-error';
import { cache } from '../../../util/cache/package/decorator';
import { regEx } from '../../../util/regex';
import { asTimestamp } from '../../../util/timestamp';
import { joinUrlParts } from '../../../util/url';
import { isVersion, id as semverVersioningId } from '../../versioning/semver';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, Release, ReleaseResult } from '../types';

const lineTerminationRegex = regEx(`\r?\n`);
const releaseBeginningChar = '\t{';
const releaseTerminationChar = '\t},';
const releaseDateRegex = regEx(
  `Date\\{(?<year>\\d+),\\s+(?<month>\\d+),\\s+(?<day>\\d+)\\}`,
);
const releaseVersionRegex = regEx(
  `Version\\{(?<versionMajor>\\d+),\\s+(?<versionMinor>\\d+),\\s+(?<patch>\\d+)\\}`,
);
const releaseFutureRegex = regEx(`Future:\\s+true`);

export class GolangVersionDatasource extends Datasource {
  static readonly id = 'golang-version';

  constructor() {
    super(GolangVersionDatasource.id);
  }

  override readonly defaultRegistryUrls = [
    'https://raw.githubusercontent.com/golang/website',
  ];

  override readonly customRegistrySupport = true;

  override readonly defaultVersioning = semverVersioningId;

  override readonly releaseTimestampSupport = true;
  override readonly releaseTimestampNote =
    'The release timestamp is determined from the `Date` field in the results.';
  override readonly sourceUrlSupport = 'package';
  override readonly sourceUrlNote =
    'We use the URL: https://github.com/golang/go.';

  @cache({ namespace: `datasource-${GolangVersionDatasource.id}`, key: 'all' })
  async getReleases({
    registryUrl,
  }: GetReleasesConfig): Promise<ReleaseResult | null> {
    /* v8 ignore next 3 -- should never happen */
    if (!registryUrl) {
      return null;
    }

    const res: ReleaseResult = {
      homepage: 'https://go.dev/',
      sourceUrl: 'https://github.com/golang/go',
      releases: [],
    };

    const golangVersionsUrl = joinUrlParts(
      registryUrl,
      '/HEAD/internal/history/release.go',
    );

    const response = await this.http.getText(golangVersionsUrl);

    const lines = response.body.split(lineTerminationRegex);

    const startOfReleases = lines.indexOf('var Releases = []*Release{');
    if (startOfReleases === -1) {
      throw new ExternalHostError(
        new Error('Invalid file - could not find the Releases section'),
      );
    }

    // Remove part before releases
    lines.splice(0, startOfReleases + 1);

    // Parse the release list
    let release: Omit<Release, 'version'> & { version?: string | undefined } = {
      version: undefined,
    };
    let skipFutureRelease = false;
    while (lines.length !== 0) {
      const line = lines.shift()!;
      if (line === releaseBeginningChar) {
        if (release.version !== undefined) {
          throw new ExternalHostError(
            new Error(
              'Invalid file - unexpected error while parsing a release',
            ),
          );
        }
      } else if (line === releaseTerminationChar) {
        if (skipFutureRelease) {
          skipFutureRelease = false;
        } else {
          if (release.version === undefined) {
            throw new ExternalHostError(
              new Error('Invalid file - release has empty version'),
            );
          }
          res.releases.push(release as Release);
        }
        release = { version: undefined };
      } else {
        const isFutureRelease = releaseFutureRegex.test(line);
        if (isFutureRelease) {
          skipFutureRelease = true;
        }
        const releaseDateMatch = releaseDateRegex.exec(line);
        if (releaseDateMatch?.groups) {
          // Make a valid UTC timestamp
          const year = releaseDateMatch.groups.year.padStart(4, '0');
          const month = releaseDateMatch.groups.month.padStart(2, '0');
          const day = releaseDateMatch.groups.day.padStart(2, '0');
          release.releaseTimestamp = asTimestamp(
            `${year}-${month}-${day}T00:00:00.000Z`,
          );
        }
        const releaseVersionMatch = releaseVersionRegex.exec(line);
        if (releaseVersionMatch?.groups) {
          release.version = `${releaseVersionMatch.groups.versionMajor}.${releaseVersionMatch.groups.versionMinor}.${releaseVersionMatch.groups.patch}`;
          if (!isVersion(release.version)) {
            throw new ExternalHostError(
              new Error(`Version ${release.version} is not a valid semver`),
            );
          }
        }
      }
    }

    if (res.releases.length === 0) {
      throw new ExternalHostError(
        new Error(`Invalid file - zero releases extracted`),
      );
    }

    return res;
  }
}
