import assert from 'node:assert';
import BoxError from './boxerror.js';
import logger from './logger.js';
import manifestFormat from '@cloudron/manifest-format';
import promiseRetry from './promise-retry.js';
import safe from 'safetydance';
import superagent from '@cloudron/superagent';

const { log } = logger('community');

const CLOUDRON_VERSIONS_FILE = 'CloudronVersions.json';

async function fetchVersionsRoot(url) {
    assert.strictEqual(typeof url, 'string');

    const [error, response] = await safe(superagent.get(url).timeout(60 * 1000).ok(() => true));
    if (error) throw new BoxError(BoxError.NETWORK_ERROR, 'Network error downloading manifest: ' + error.message);
    if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);

    // if the content-type is incorrect, we will get a buffer
    const versionsRoot = Buffer.isBuffer(response.body) ? safe.JSON.parse(response.body.toString('utf8')) : response.body;
    const versionsError = manifestFormat.parseVersions(versionsRoot);
    if (versionsError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid CloudronVersions.json: ${versionsError.message}`);

    return versionsRoot;
}

function getVersionData(versionsRoot, version) {
    assert.strictEqual(typeof versionsRoot, 'object');
    assert.strictEqual(typeof version, 'string');

    const versions = versionsRoot.versions;
    const sortedVersions = Object.keys(versions).filter(v => versions[v].publishState !== 'revoked').sort(manifestFormat.packageVersionCompare);
    const versionData = version === 'latest' ? versions[sortedVersions.at(-1)] : versions[version];
    if (!versionData) throw new BoxError(BoxError.NOT_FOUND, `Version ${version} not found`);

    const manifestError = manifestFormat.checkVersionsRequirements(versionData.manifest);
    if (manifestError) throw new BoxError(BoxError.BAD_FIELD, `Invalid manifest: ${manifestError.message}`);

    const unstable = !versionsRoot.stable || versionData.publishState !== 'published';

    return { ...versionData, unstable }; // { manifest, publishState, creationDate, ts, unstable }
}

// github: https://github.com/cloudron/test-app/blob/main/CloudronVersions.json
//   raw: https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json
// gitlab: https://gitlab.com/gitlab-org/gitlab/-/blob/master/CloudronVersions.json?ref_type=heads
//   raw: https://gitlab.com/gitlab-org/gitlab/-/raw/master/CloudronVersions.json
// codeberg: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CloudronVersions.json
//   raw: https://codeberg.org/forgejo/forgejo/raw/branch/forgejo/CloudronVersions.json
// gitea: https://git.selfhosted.in/girish/configs/src/branch/master/CloudronVersions.json
//   raw: https://git.selfhosted.in/girish/configs/raw/branch/master/CloudronVersions.json
// surfer: https://lotsof-space.de/cloudron/webmail/CloudronVersions.json
function _getUrlCandidates(parsedUrl) {
    assert.strictEqual(typeof parsedUrl, 'object');

    const candidates = [];
    let url = parsedUrl.origin + parsedUrl.pathname; // strip query params

    if (!url.endsWith(`/${CLOUDRON_VERSIONS_FILE}`)) url += url.endsWith('/') ? CLOUDRON_VERSIONS_FILE : `/${CLOUDRON_VERSIONS_FILE}`;

    candidates.push(url);

    if (url.startsWith('https://raw.githubusercontent.com/')) return candidates;

    if (url.startsWith('https://github.com/') && url.includes('/blob/')) {
        const match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/);
        if (match) candidates.push(`https://raw.githubusercontent.com/${match[1]}/${match[2]}/${match[3]}`);
    } else if (url.includes('/-/blob/')) { // gitlab (hosted and self-hosted)
        candidates.push(url.replace('/-/blob/', '/-/raw/'));
    } else if (/\/src\/(branch|tag|commit)\//.test(url)) { // gitea/codeberg/gogs/forgejo
        candidates.push(url.replace('/src/', '/raw/'));
    } else if (/\b(git|gitlab|gitea|forgejo|gogs|codeberg|code)\b/.test(parsedUrl.hostname)) {
        const repoBase = url.slice(0, -CLOUDRON_VERSIONS_FILE.length - 1);
        candidates.push(`${repoBase}/-/raw/master/${CLOUDRON_VERSIONS_FILE}`);
        candidates.push(`${repoBase}/-/raw/main/${CLOUDRON_VERSIONS_FILE}`);
        candidates.push(`${repoBase}/raw/branch/master/${CLOUDRON_VERSIONS_FILE}`);
        candidates.push(`${repoBase}/raw/branch/main/${CLOUDRON_VERSIONS_FILE}`);
    }

    return candidates;
}

async function resolveVersionsUrl(url, version = 'latest') {
    assert.strictEqual(typeof url, 'string');
    assert.strictEqual(typeof version, 'string');

    const parsedUrl = safe(() => new URL(url));
    if (!parsedUrl) throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl is not a valid URL');
    if (parsedUrl.protocol !== 'https:') throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl must use https');

    const candidates = _getUrlCandidates(parsedUrl);

    for (const candidate of candidates) {
        const [error, versionsRoot] = await safe(fetchVersionsRoot(candidate));
        if (!error) {
            const versionData = getVersionData(versionsRoot, version);
            return { ...versionData, resolvedUrl: candidate, versionsUrl: `${candidate}@${version}` };
        }
    }

    throw new BoxError(BoxError.NOT_FOUND, 'Could not resolve CloudronVersions.json from URL');
}

async function getAppVersion(url, version) {
    assert.strictEqual(typeof url, 'string');
    assert.strictEqual(typeof version, 'string');

    if (!url.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'URL must use HTTPS');

    const versionsRoot = await fetchVersionsRoot(url);
    return getVersionData(versionsRoot, version);
}

async function downloadManifest(versionsUrl) {
    assert.strictEqual(typeof versionsUrl, 'string');

    const atIndex = versionsUrl.lastIndexOf('@');
    if (atIndex === -1) throw new BoxError(BoxError.BAD_FIELD, 'version is required in versionsUrl (format: url@version)');

    const url = versionsUrl.substring(0, atIndex);
    const version = versionsUrl.substring(atIndex + 1);

    if (!url.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl must use https');
    if (!version) throw new BoxError(BoxError.BAD_FIELD, 'version is required in versionsUrl (format: url@version)');

    log(`downloading manifest from ${url} version ${version}`);

    const versionsRoot = await fetchVersionsRoot(url);

    const versions = versionsRoot.versions;
    const sortedVersions = Object.keys(versions).filter(v => versions[v].publishState !== 'revoked').sort(manifestFormat.packageVersionCompare);
    const versionData = version === 'latest' ? versions[sortedVersions.at(-1)] : versions[version];

    if (!versionData) throw new BoxError(BoxError.NOT_FOUND, `Version ${version} not found`);
    if (!versionData.manifest || typeof versionData.manifest !== 'object') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Missing manifest in version data');

    return { versionsUrl: url, manifest: versionData.manifest };
}

async function getAppUpdate(app, options) {
    assert.strictEqual(typeof app, 'object');
    assert.strictEqual(typeof options, 'object');

    const [error, response] = await safe(superagent.get(app.versionsUrl).timeout(60 * 1000).ok(() => true));
    if (error) throw new BoxError(BoxError.NETWORK_ERROR, 'Network error downloading manifest: ' + error.message);
    if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);

    // if the content-type is incorrect, we will get a buffer
    const versionsRoot = Buffer.isBuffer(response.body) ? safe.JSON.parse(response.body.toString('utf8')) : response.body;
    const versionsError = manifestFormat.parseVersions(versionsRoot);
    if (versionsError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid CloudronVersions.json: ${versionsError.message}`);

    const versions = versionsRoot.versions;
    const sortedVersions = Object.keys(versions).filter(v => versions[v].publishState !== 'revoked').sort(manifestFormat.packageVersionCompare);
    const idx = sortedVersions.findIndex(v => v === app.manifest.version);
    if (idx === -1) return null; // current version was revoked or not found, no update info available
    if (idx === sortedVersions.length-1) return null; // no update
    const nextVersion = versions[sortedVersions[idx+1]];
    const unstable = !versionsRoot.stable || nextVersion.publishState !== 'published';

    return {
        id: app.manifest.id,
        creationDate: nextVersion.creationDate,
        manifest: nextVersion.manifest,
        unstable
    };
}

async function downloadIcon(manifest) {
    return await promiseRetry({ times: 10, interval: 5000, log }, async function () {
        const [networkError, response] = await safe(superagent.get(manifest.iconUrl)
            .timeout(60 * 1000)
            .ok(() => true));

        if (networkError) throw new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon: ${networkError.message}`);
        if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Icon download failed. ${response.status} ${JSON.stringify(response.body)}`);

        if (!Buffer.isBuffer(response.body)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid icon returned for app');

        return response.body;
    });
}

export default {
    _getUrlCandidates,
    resolveVersionsUrl,
    getAppVersion,
    downloadManifest,
    getAppUpdate,
    downloadIcon
};
