const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');

function parseArgs(argv) {
  const args = { _: [] };

  for (let index = 0; index < argv.length; index += 1) {
    const token = argv[index];

    if (!token.startsWith('--')) {
      args._.push(token);
      continue;
    }

    const key = token.slice(2);
    const nextToken = argv[index + 1];

    if (!nextToken || nextToken.startsWith('--')) {
      args[key] = true;
      continue;
    }

    args[key] = nextToken;
    index += 1;
  }

  return args;
}

function requireArg(args, key) {
  const value = args[key] ?? process.env[key.toUpperCase().replace(/-/g, '_')];

  if (typeof value !== 'string' || value.trim().length === 0) {
    throw new Error(`Missing required argument: --${key}`);
  }

  return value.trim();
}

function resolveToken(args) {
  const token = args.token
    || process.env.GITEA_RELEASE_TOKEN
    || process.env.GITHUB_TOKEN;

  if (typeof token !== 'string' || token.trim().length === 0) {
    throw new Error('A Gitea API token is required. Set GITEA_RELEASE_TOKEN or pass --token.');
  }

  return token.trim();
}

function normalizeServerUrl(rawValue) {
  const parsedUrl = new URL(rawValue.trim());

  if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
    throw new Error('The Gitea server URL must use http:// or https://.');
  }

  return parsedUrl.toString().replace(/\/$/, '');
}

function parseRepository(rawValue) {
  const trimmedValue = rawValue.trim();
  const [owner, ...repoParts] = trimmedValue.split('/');

  if (!owner || repoParts.length === 0) {
    throw new Error(`Repository must be in OWNER/REPO format. Received: ${rawValue}`);
  }

  return {
    owner,
    repo: repoParts.join('/')
  };
}

function buildApiUrl(serverUrl, repository, apiPath, query = {}) {
  const { owner, repo } = parseRepository(repository);
  const pathname = apiPath
    .replace('{owner}', encodeURIComponent(owner))
    .replace('{repo}', encodeURIComponent(repo));
  const url = new URL(`${serverUrl}/api/v1${pathname}`);

  for (const [key, value] of Object.entries(query)) {
    if (value === undefined || value === null || value === '') {
      continue;
    }

    url.searchParams.set(key, String(value));
  }

  return url;
}

function buildAuthHeaders(token) {
  return {
    Authorization: `token ${token}`,
    'X-Gitea-Token': token
  };
}

function sendRequest({
  body,
  expectedStatuses = [200],
  headers = {},
  maxRedirects = 5,
  method,
  token,
  url
}) {
  return new Promise((resolve, reject) => {
    const requestUrl = typeof url === 'string' ? new URL(url) : url;
    const transport = requestUrl.protocol === 'https:' ? https : http;
    const request = transport.request({
      method,
      hostname: requestUrl.hostname,
      port: requestUrl.port || undefined,
      path: `${requestUrl.pathname}${requestUrl.search}`,
      headers: {
        Accept: 'application/json',
        ...buildAuthHeaders(token),
        ...headers
      }
    }, (response) => {
      const statusCode = response.statusCode || 0;
      const location = response.headers.location;

      if (statusCode >= 300 && statusCode < 400 && location && maxRedirects > 0) {
        response.resume();
        sendRequest({
          body,
          expectedStatuses,
          headers,
          maxRedirects: maxRedirects - 1,
          method,
          token,
          url: new URL(location, requestUrl)
        }).then(resolve, reject);
        return;
      }

      const chunks = [];

      response.on('data', (chunk) => chunks.push(chunk));
      response.on('end', () => {
        const responseBody = Buffer.concat(chunks);

        if (!expectedStatuses.includes(statusCode)) {
          reject(new Error(`${method} ${requestUrl} failed with status ${statusCode}: ${responseBody.toString('utf8')}`));
          return;
        }

        resolve({
          body: responseBody,
          headers: response.headers,
          statusCode
        });
      });
    });

    request.on('error', reject);

    if (body) {
      request.end(body);
      return;
    }

    request.end();
  });
}

async function sendJsonRequest({ expectedStatuses, jsonBody, method, repository, serverUrl, token, urlPath }) {
  const body = jsonBody ? Buffer.from(JSON.stringify(jsonBody), 'utf8') : undefined;
  const response = await sendRequest({
    body,
    expectedStatuses,
    headers: jsonBody ? {
      'Content-Length': body.length,
      'Content-Type': 'application/json'
    } : undefined,
    method,
    token,
    url: buildApiUrl(serverUrl, repository, urlPath)
  });
  const responseText = response.body.toString('utf8').trim();

  return responseText.length > 0 ? JSON.parse(responseText) : null;
}

async function sendJsonRequestWithQuery({ expectedStatuses, jsonBody, method, query, repository, serverUrl, token, urlPath }) {
  const body = jsonBody ? Buffer.from(JSON.stringify(jsonBody), 'utf8') : undefined;
  const response = await sendRequest({
    body,
    expectedStatuses,
    headers: jsonBody ? {
      'Content-Length': body.length,
      'Content-Type': 'application/json'
    } : undefined,
    method,
    token,
    url: buildApiUrl(serverUrl, repository, urlPath, query)
  });
  const responseText = response.body.toString('utf8').trim();

  return responseText.length > 0 ? JSON.parse(responseText) : null;
}

async function getReleaseByTag({ repository, serverUrl, tag, token }) {
  try {
    return await sendJsonRequest({
      expectedStatuses: [200],
      method: 'GET',
      repository,
      serverUrl,
      token,
      urlPath: `/repos/{owner}/{repo}/releases/tags/${encodeURIComponent(tag)}`
    });
  } catch (error) {
    if (error instanceof Error && /status 404/.test(error.message)) {
      return null;
    }

    throw error;
  }
}

async function getReleaseById({ id, repository, serverUrl, token }) {
  return sendJsonRequest({
    expectedStatuses: [200],
    method: 'GET',
    repository,
    serverUrl,
    token,
    urlPath: `/repos/{owner}/{repo}/releases/${encodeURIComponent(String(id))}`
  });
}

async function createRelease({ body, draft, name, prerelease, repository, serverUrl, tag, target, token }) {
  return sendJsonRequest({
    expectedStatuses: [201],
    jsonBody: {
      body,
      draft,
      name,
      prerelease,
      tag_name: tag,
      target_commitish: target
    },
    method: 'POST',
    repository,
    serverUrl,
    token,
    urlPath: '/repos/{owner}/{repo}/releases'
  });
}

async function updateRelease({ body, draft, id, name, prerelease, repository, serverUrl, tag, target, token }) {
  return sendJsonRequest({
    expectedStatuses: [200],
    jsonBody: {
      body,
      draft,
      name,
      prerelease,
      tag_name: tag,
      target_commitish: target
    },
    method: 'PATCH',
    repository,
    serverUrl,
    token,
    urlPath: `/repos/{owner}/{repo}/releases/${encodeURIComponent(String(id))}`
  });
}

async function listReleaseAssets({ releaseId, repository, serverUrl, token }) {
  return sendJsonRequest({
    expectedStatuses: [200],
    method: 'GET',
    repository,
    serverUrl,
    token,
    urlPath: `/repos/{owner}/{repo}/releases/${encodeURIComponent(String(releaseId))}/assets`
  });
}

async function listReleases({ draft, limit, repository, serverUrl, token }) {
  return sendJsonRequestWithQuery({
    expectedStatuses: [200],
    method: 'GET',
    query: {
      draft,
      limit,
      page: 1
    },
    repository,
    serverUrl,
    token,
    urlPath: '/repos/{owner}/{repo}/releases'
  });
}

async function deleteReleaseAsset({ attachmentId, releaseId, repository, serverUrl, token }) {
  await sendRequest({
    expectedStatuses: [204],
    method: 'DELETE',
    token,
    url: buildApiUrl(
      serverUrl,
      repository,
      `/repos/{owner}/{repo}/releases/${encodeURIComponent(String(releaseId))}/assets/${encodeURIComponent(String(attachmentId))}`
    )
  });
}

function uploadReleaseAsset({ filePath, releaseId, repository, serverUrl, token }) {
  return new Promise((resolve, reject) => {
    const fileName = path.basename(filePath);
    const boundary = `----MetoYouRelease${Date.now().toString(16)}`;
    const preamble = Buffer.from(
      `--${boundary}\r\nContent-Disposition: form-data; name="attachment"; filename="${fileName.replace(/"/g, '')}"\r\nContent-Type: application/octet-stream\r\n\r\n`,
      'utf8'
    );
    const closing = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
    const fileSize = fs.statSync(filePath).size;
    const uploadUrl = buildApiUrl(
      serverUrl,
      repository,
      `/repos/{owner}/{repo}/releases/${encodeURIComponent(String(releaseId))}/assets`,
      { name: fileName }
    );
    const transport = uploadUrl.protocol === 'https:' ? https : http;
    const request = transport.request({
      method: 'POST',
      hostname: uploadUrl.hostname,
      port: uploadUrl.port || undefined,
      path: `${uploadUrl.pathname}${uploadUrl.search}`,
      headers: {
        Accept: 'application/json',
        ...buildAuthHeaders(token),
        'Content-Length': preamble.length + fileSize + closing.length,
        'Content-Type': `multipart/form-data; boundary=${boundary}`
      }
    }, (response) => {
      const chunks = [];

      response.on('data', (chunk) => chunks.push(chunk));
      response.on('end', () => {
        const statusCode = response.statusCode || 0;
        const responseBody = Buffer.concat(chunks).toString('utf8');

        if (statusCode !== 201) {
          reject(new Error(`Failed to upload ${fileName}: ${statusCode} ${responseBody}`));
          return;
        }

        resolve(responseBody.trim().length > 0 ? JSON.parse(responseBody) : null);
      });
    });

    request.on('error', reject);
    request.write(preamble);

    const stream = fs.createReadStream(filePath);
    stream.on('error', (error) => {
      request.destroy(error);
      reject(error);
    });
    stream.on('end', () => request.end(closing));
    stream.pipe(request, { end: false });
  });
}

async function downloadFile({ filePath, token, url }) {
  const response = await sendRequest({
    expectedStatuses: [200],
    method: 'GET',
    token,
    url
  });

  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, response.body);
}

function writeGitHubOutputs(entries) {
  if (!process.env.GITHUB_OUTPUT) {
    return;
  }

  for (const [key, value] of Object.entries(entries)) {
    fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`, 'utf8');
  }
}

function toDownloadUrl(serverUrl, repository, tag) {
  return `${serverUrl}/${repository}/releases/download/${encodeURIComponent(tag)}`;
}

function collectTopLevelFiles(directoryPath) {
  if (!fs.existsSync(directoryPath)) {
    return [];
  }

  return fs.readdirSync(directoryPath, { withFileTypes: true })
    .filter((entry) => entry.isFile())
    .map((entry) => path.join(directoryPath, entry.name));
}

function isDesktopReleaseAsset(filePath) {
  const fileName = path.basename(filePath);
  const lowerFileName = fileName.toLowerCase();

  return fileName === 'latest.yml'
    || fileName === 'latest-linux.yml'
    || fileName === 'latest-mac.yml'
    || fileName === 'release-manifest.json'
    || lowerFileName.endsWith('.appimage')
    || lowerFileName.endsWith('.blockmap')
    || lowerFileName.endsWith('.deb')
    || lowerFileName.endsWith('.dmg')
    || lowerFileName.endsWith('.exe')
    || lowerFileName.endsWith('.zip');
}

function collectBuiltAssets(args) {
  const distElectronDir = path.resolve(process.cwd(), args['dist-electron'] || 'dist-electron');
  const distServerDir = path.resolve(process.cwd(), args['dist-server'] || 'dist-server');
  const files = [
    ...collectTopLevelFiles(distElectronDir).filter(isDesktopReleaseAsset),
    ...collectTopLevelFiles(distServerDir)
  ];

  return [...new Set(files)].sort((left, right) => left.localeCompare(right));
}

async function ensureDraftReleaseCommand(args) {
  const token = resolveToken(args);
  const serverUrl = normalizeServerUrl(requireArg(args, 'server-url'));
  const repository = requireArg(args, 'repository');
  const tag = requireArg(args, 'tag');
  const target = requireArg(args, 'target');
  const name = requireArg(args, 'name');
  const body = typeof args.body === 'string' ? args.body : '';
  const existingRelease = await getReleaseByTag({ repository, serverUrl, tag, token });
  const release = existingRelease
    ? await updateRelease({
      body,
      draft: Boolean(existingRelease.draft),
      id: existingRelease.id,
      name,
      prerelease: Boolean(existingRelease.prerelease),
      repository,
      serverUrl,
      tag,
      target,
      token
    })
    : await createRelease({
      body,
      draft: true,
      name,
      prerelease: false,
      repository,
      serverUrl,
      tag,
      target,
      token
    });
  const outputs = {
    release_download_url: toDownloadUrl(serverUrl, repository, release.tag_name),
    release_html_url: release.html_url,
    release_id: String(release.id),
    release_tag: release.tag_name
  };

  if (args['write-output']) {
    writeGitHubOutputs(outputs);
  }

  console.log(JSON.stringify({
    ...outputs,
    release_name: release.name
  }, null, 2));
}

async function downloadLatestManifestCommand(args) {
  const token = resolveToken(args);
  const serverUrl = normalizeServerUrl(requireArg(args, 'server-url'));
  const repository = requireArg(args, 'repository');
  const outputPath = path.resolve(process.cwd(), requireArg(args, 'output'));
  const allowMissing = Boolean(args['allow-missing']);
  const releases = await listReleases({
    draft: false,
    limit: 20,
    repository,
    serverUrl,
    token
  });

  for (const release of Array.isArray(releases) ? releases : []) {
    if (!release || release.draft) {
      continue;
    }

    const assets = await listReleaseAssets({
      releaseId: release.id,
      repository,
      serverUrl,
      token
    });
    const manifestAsset = Array.isArray(assets)
      ? assets.find((asset) => asset && asset.name === 'release-manifest.json')
      : null;

    if (!manifestAsset) {
      continue;
    }

    await downloadFile({
      filePath: outputPath,
      token,
      url: manifestAsset.browser_download_url
    });

    console.log(`[gitea-release] Downloaded ${manifestAsset.name} from ${release.tag_name}`);
    return;
  }

  if (allowMissing) {
    console.log('[gitea-release] No published release manifest was found. Continuing without a previous manifest.');
    return;
  }

  throw new Error('No published release-manifest.json asset was found.');
}

async function uploadBuiltAssetsCommand(args) {
  const token = resolveToken(args);
  const serverUrl = normalizeServerUrl(requireArg(args, 'server-url'));
  const repository = requireArg(args, 'repository');
  const releaseId = requireArg(args, 'release-id');
  const files = collectBuiltAssets(args);

  if (files.length === 0) {
    throw new Error('No built assets were found to upload.');
  }

  const existingAssets = await listReleaseAssets({
    releaseId,
    repository,
    serverUrl,
    token
  });
  const existingAssetsByName = new Map(
    (Array.isArray(existingAssets) ? existingAssets : []).map((asset) => [asset.name, asset])
  );

  for (const filePath of files) {
    const fileName = path.basename(filePath);
    const existingAsset = existingAssetsByName.get(fileName);

    if (existingAsset) {
      await deleteReleaseAsset({
        attachmentId: existingAsset.id,
        releaseId,
        repository,
        serverUrl,
        token
      });
    }

    await uploadReleaseAsset({
      filePath,
      releaseId,
      repository,
      serverUrl,
      token
    });

    console.log(`[gitea-release] Uploaded ${fileName}`);
  }
}

async function publishReleaseCommand(args) {
  const token = resolveToken(args);
  const serverUrl = normalizeServerUrl(requireArg(args, 'server-url'));
  const repository = requireArg(args, 'repository');
  const tag = requireArg(args, 'tag');
  const release = await getReleaseByTag({ repository, serverUrl, tag, token });

  if (!release) {
    throw new Error(`Release ${tag} was not found.`);
  }

  const publishedRelease = release.draft
    ? await updateRelease({
      body: release.body || '',
      draft: false,
      id: release.id,
      name: release.name || release.tag_name,
      prerelease: Boolean(release.prerelease),
      repository,
      serverUrl,
      tag: release.tag_name,
      target: release.target_commitish,
      token
    })
    : release;

  if (args['write-output']) {
    writeGitHubOutputs({
      release_html_url: publishedRelease.html_url,
      release_id: String(publishedRelease.id),
      release_tag: publishedRelease.tag_name
    });
  }

  console.log(JSON.stringify({
    release_html_url: publishedRelease.html_url,
    release_id: publishedRelease.id,
    release_tag: publishedRelease.tag_name
  }, null, 2));
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const command = args._[0];

  switch (command) {
    case 'ensure-draft':
      await ensureDraftReleaseCommand(args);
      return;
    case 'download-latest-manifest':
      await downloadLatestManifestCommand(args);
      return;
    case 'upload-built-assets':
      await uploadBuiltAssetsCommand(args);
      return;
    case 'publish':
      await publishReleaseCommand(args);
      return;
    default:
      throw new Error(`Unsupported command: ${command || '(none)'}`);
  }
}

main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);

  console.error(`[gitea-release] ${message}`);
  process.exitCode = 1;
});
