import { logger } from '../logging.ts';
import { projectInfo } from '../info.ts';
import { getErrorMessage } from '../utils/error.ts';
import * as interfaces from '../../ts_interfaces/index.ts';

const ONEBOX_REPOSITORY_URL = 'https://code.foss.global/serve.zone/onebox';
const ONEBOX_LATEST_RELEASE_API_URL =
  'https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/latest';
const ONEBOX_INSTALL_SCRIPT_URL = `${ONEBOX_REPOSITORY_URL}/raw/branch/main/install.sh`;
const ONEBOX_CHANGELOG_URL = `${ONEBOX_REPOSITORY_URL}/src/branch/main/changelog.md`;
const UPGRADE_LOG_PATH = '/var/log/onebox-upgrade.log';

interface IGiteaReleaseResponse {
  tag_name?: unknown;
  html_url?: unknown;
}

interface IParsedRelease {
  tagName: string;
  releaseUrl: string;
}

export class OneboxUpdateManager {
  private cachedStatus: interfaces.data.IOneboxUpdateStatus | null = null;
  private cachedStatusExpiresAt = 0;
  private upgradeStartedAt = 0;
  private readonly statusCacheTtlMs = 5 * 60 * 1000;

  public async getUpdateStatus(
    optionsArg: { force?: boolean } = {},
  ): Promise<interfaces.data.IOneboxUpdateStatus> {
    const now = Date.now();
    if (!optionsArg.force && this.cachedStatus && this.cachedStatusExpiresAt > now) {
      return this.cachedStatus;
    }

    const status = await this.fetchUpdateStatus();
    this.cachedStatus = status;
    this.cachedStatusExpiresAt = now + this.statusCacheTtlMs;
    return status;
  }

  public async startDetachedUpgrade(): Promise<interfaces.data.IOneboxUpgradeStartResult> {
    this.assertRoot();

    const status = await this.getUpdateStatus({ force: true });
    this.assertUpdateCheckSucceeded(status);

    const targetVersion = status.latestVersion || status.currentVersion;
    if (!status.updateAvailable) {
      return {
        accepted: false,
        currentVersion: status.currentVersion,
        targetVersion,
        message: 'Onebox is already up to date.',
      };
    }

    if (this.upgradeStartedAt && Date.now() - this.upgradeStartedAt < 10 * 60 * 1000) {
      return {
        accepted: false,
        currentVersion: status.currentVersion,
        targetVersion,
        message: 'A Onebox upgrade has already been started.',
        logPath: UPGRADE_LOG_PATH,
      };
    }

    const unitName = `onebox-upgrade-${Date.now()}`;
    const command = new Deno.Command('systemd-run', {
      args: [
        '--unit',
        unitName,
        '--description',
        `Onebox upgrade to ${targetVersion}`,
        '--collect',
        '--property=Type=oneshot',
        '--setenv=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
        'bash',
        '-lc',
        this.createDetachedUpgradeScript(),
      ],
      stdin: 'null',
      stdout: 'piped',
      stderr: 'piped',
    });
    const result = await command.output();
    if (!result.success) {
      const stderr = new TextDecoder().decode(result.stderr).trim();
      const stdout = new TextDecoder().decode(result.stdout).trim();
      throw new Error(
        `Failed to start Onebox upgrade systemd unit: ${
          stderr || stdout || `exit code ${result.code}`
        }`,
      );
    }
    this.upgradeStartedAt = Date.now();

    logger.info(`Started Onebox upgrade systemd unit ${unitName}`);
    return {
      accepted: true,
      currentVersion: status.currentVersion,
      targetVersion,
      message: 'Onebox upgrade started. The service will restart automatically.',
      unitName,
      logPath: UPGRADE_LOG_PATH,
    };
  }

  public async runUpgradeForeground(
    statusArg?: interfaces.data.IOneboxUpdateStatus,
  ): Promise<interfaces.data.IOneboxUpgradeStartResult> {
    this.assertRoot();

    const status = statusArg || (await this.getUpdateStatus({ force: true }));
    this.assertUpdateCheckSucceeded(status);

    const targetVersion = status.latestVersion || status.currentVersion;
    if (!status.updateAvailable) {
      return {
        accepted: false,
        currentVersion: status.currentVersion,
        targetVersion,
        message: 'Onebox is already up to date.',
      };
    }

    const installCommand = new Deno.Command('bash', {
      args: ['-c', `set -o pipefail; curl -fsSL ${ONEBOX_INSTALL_SCRIPT_URL} | bash`],
      stdin: 'inherit',
      stdout: 'inherit',
      stderr: 'inherit',
    });
    const installResult = await installCommand.output();
    if (!installResult.success) {
      throw new Error('Upgrade failed');
    }

    return {
      accepted: true,
      currentVersion: status.currentVersion,
      targetVersion,
      message: `Upgraded to ${targetVersion}`,
    };
  }

  private async fetchUpdateStatus(): Promise<interfaces.data.IOneboxUpdateStatus> {
    const currentVersion = this.normalizeVersion(projectInfo.version);
    const checkedAt = Date.now();

    try {
      const release = await this.fetchLatestRelease();
      const latestVersion = this.normalizeVersion(release.tagName);
      return {
        currentVersion,
        latestVersion,
        updateAvailable: currentVersion !== latestVersion,
        checkedAt,
        releaseUrl: release.releaseUrl,
        changelogUrl: ONEBOX_CHANGELOG_URL,
      };
    } catch (error) {
      return {
        currentVersion,
        latestVersion: null,
        updateAvailable: false,
        checkedAt,
        releaseUrl: `${ONEBOX_REPOSITORY_URL}/releases`,
        changelogUrl: ONEBOX_CHANGELOG_URL,
        error: getErrorMessage(error),
      };
    }
  }

  private async fetchLatestRelease(): Promise<IParsedRelease> {
    const abortController = new AbortController();
    const timeoutId = setTimeout(() => abortController.abort(), 5000);

    try {
      const response = await fetch(ONEBOX_LATEST_RELEASE_API_URL, {
        headers: { accept: 'application/json' },
        signal: abortController.signal,
      });
      if (!response.ok) {
        throw new Error(`Failed to fetch latest release: HTTP ${response.status}`);
      }

      const release = await response.json() as IGiteaReleaseResponse;
      if (typeof release.tag_name !== 'string' || !release.tag_name) {
        throw new Error('Latest release response does not include a tag name');
      }

      const tagName = release.tag_name;
      const releaseUrl = typeof release.html_url === 'string' && release.html_url
        ? release.html_url
        : `${ONEBOX_REPOSITORY_URL}/releases/tag/${this.normalizeVersion(tagName)}`;

      return { tagName, releaseUrl };
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private assertRoot(): void {
    if (Deno.uid() !== 0) {
      throw new Error('Onebox upgrades must be started as root. Try: sudo onebox upgrade');
    }
  }

  private assertUpdateCheckSucceeded(statusArg: interfaces.data.IOneboxUpdateStatus): void {
    if (statusArg.error) {
      throw new Error(`Cannot determine latest Onebox release: ${statusArg.error}`);
    }
  }

  private normalizeVersion(versionArg: string): string {
    const trimmedVersion = versionArg.trim();
    return trimmedVersion.startsWith('v') ? trimmedVersion : `v${trimmedVersion}`;
  }

  private createDetachedUpgradeScript(): string {
    return `
set -e
set -o pipefail
mkdir -p /var/log
{
  echo "==== Onebox upgrade started $(date -Is) ===="
  sleep 2
  curl -fsSL ${ONEBOX_INSTALL_SCRIPT_URL} | bash
  echo "==== Onebox upgrade finished $(date -Is) ===="
} >> ${UPGRADE_LOG_PATH} 2>&1
`;
  }
}
