/**
 * Plugin update delivery.
 *
 * Contract: plugins/international-press-zone/docs/specs/
 * 2026-08-11-plugin-update-delivery-design.md
 */

import { PrismaClient } from '@prisma/client';
import { getR2Config } from '../config/r2';
import { presignGetObjectUrl } from '../utils/r2Presigner';
import {
  hashLicenseKey,
  normalizeSiteUrl,
  validateLicense,
} from './multilingualLicenseService';

const prisma = new PrismaClient();

export type EntitledLicense = {
  id: string;
  plan_tier: string;
};

export type UpdateCheckResult =
  | { update_available: false }
  | {
      update_available: true;
      version: string;
      url: string;
      package: string;
      package_expires_in: number;
      tested: string;
      requires: string;
      requires_php: string;
      description: string;
      changelog: string;
      banners: Record<string, string>;
      icons: Record<string, string>;
      sha256: string;
      signature: string;
      signature_key_id: string;
    };

export type PackageVerificationResult =
  | { valid: false }
  | {
      valid: true;
      version: string;
      sha256: string;
      signature: string;
      signature_key_id: string;
    };

/** Numeric dotted-version comparison. Lexical comparison ranks 1.9.0 above 1.10.0. */
export function compareVersions(a: string, b: string): number {
  const parse = (value: string): number[] =>
    value
      .trim()
      .split(/[.\-+]/)
      .map((part) => Number.parseInt(part, 10))
      .map((part) => (Number.isFinite(part) ? part : 0));

  const left = parse(a);
  const right = parse(b);
  const length = Math.max(left.length, right.length);

  for (let index = 0; index < length; index += 1) {
    const difference = (left[index] ?? 0) - (right[index] ?? 0);
    if (difference !== 0) {
      return difference > 0 ? 1 : -1;
    }
  }

  return 0;
}

/**
 * Resolve the license that may receive updates for this site.
 *
 * Status semantics come from validateLicense so this path answers unknown and
 * invalid licenses exactly as activate/validate/status already do.
 */
export async function resolveEntitledLicense(params: {
  license_key: string;
  site_url: string;
  plugin: string;
}): Promise<EntitledLicense> {
  const info = await validateLicense({
    license_key: params.license_key,
    site_url: params.site_url,
    plugin: params.plugin,
  });

  if (info.status === 'expired') {
    throw new Error('LICENSE_EXPIRED');
  }

  if (info.status !== 'active') {
    throw new Error('LICENSE_NOT_ACTIVE');
  }

  const license = await prisma.license.findUnique({
    where: { key_hash: hashLicenseKey(params.license_key) },
    include: {
      activations: {
        where: { deactivated_at: null },
        select: { site_url: true },
      },
    },
  });

  if (!license) {
    throw new Error('INVALID_LICENSE');
  }

  const requestedSite = normalizeSiteUrl(params.site_url);
  const boundToSite = license.activations.some(
    (activation) => normalizeSiteUrl(activation.site_url) === requestedSite
  );

  if (!boundToSite) {
    throw new Error('SITE_NOT_ACTIVATED');
  }

  return { id: license.id, plan_tier: license.plan_tier };
}

function signPackageUrl(objectKey: string): { url: string; expiresIn: number } {
  const r2 = getR2Config();

  if (!r2) {
    throw new Error('UPDATE_STORAGE_UNAVAILABLE');
  }

  return {
    url: presignGetObjectUrl({
      endpoint: r2.endpoint,
      bucket: r2.bucket,
      objectKey,
      region: r2.region,
      accessKeyId: r2.accessKeyId,
      secretAccessKey: r2.secretAccessKey,
      expiresIn: r2.signedUrlTtlSeconds,
    }),
    expiresIn: r2.signedUrlTtlSeconds,
  };
}

export async function checkForUpdate(params: {
  license_key: string;
  site_url: string;
  plugin: string;
  product: string;
  version: string;
}): Promise<UpdateCheckResult> {
  const license = await resolveEntitledLicense(params);

  // Publish order and version order diverge whenever a patch is backported
  // after a newer minor, so the highest version wins, not the newest row.
  const candidates = await prisma.pluginRelease.findMany({
    where: {
      plugin: params.plugin,
      product: params.product,
      is_stable: true,
      entitlements: { some: { plan_tier: license.plan_tier } },
    },
    orderBy: { published_at: 'desc' },
  });

  const release = candidates.reduce<(typeof candidates)[number] | null>(
    (highest, candidate) =>
      highest === null || compareVersions(candidate.version, highest.version) > 0
        ? candidate
        : highest,
    null
  );

  if (!release || compareVersions(release.version, params.version) <= 0) {
    return { update_available: false };
  }

  const signed = signPackageUrl(release.r2_object_key);

  return {
    update_available: true,
    version: release.version,
    url: release.homepage_url,
    package: signed.url,
    package_expires_in: signed.expiresIn,
    tested: release.tested_wp,
    requires: release.requires_wp,
    requires_php: release.requires_php,
    description: release.description,
    changelog: release.changelog,
    banners: {},
    icons: {},
    sha256: release.package_sha256,
    signature: release.package_signature,
    signature_key_id: release.signature_key_id,
  };
}

export async function verifyPackage(params: {
  license_key: string;
  site_url: string;
  plugin: string;
  product: string;
  hash: string;
  version: string;
}): Promise<PackageVerificationResult> {
  const license = await resolveEntitledLicense(params);

  // The plugin sends its installed version, not the downloaded one, so the
  // digest is the only sound way to identify the release.
  const release = await prisma.pluginRelease.findFirst({
    where: {
      package_sha256: params.hash.toLowerCase(),
      plugin: params.plugin,
      product: params.product,
      is_stable: true,
      entitlements: { some: { plan_tier: license.plan_tier } },
    },
  });

  if (!release || compareVersions(release.version, params.version) <= 0) {
    return { valid: false };
  }

  return {
    valid: true,
    version: release.version,
    sha256: release.package_sha256,
    signature: release.package_signature,
    signature_key_id: release.signature_key_id,
  };
}
