import type { Repository } from '../db/repository.js';
import {
  installSpacePackages,
  parseAndValidateSpec,
  spaceKeyFor,
  type ParsedSpec,
  type PythonPackagesConfig,
} from '../engine/python-packages.js';

/**
 * Shared per-space Python package overlay service.
 *
 * Both entry points that add a package to a space MUST go through here so they
 * share ONE module-level mutex (`spaceLocks`) and ONE desired-state read-modify-
 * write path:
 *   - the admin form  (space-python-packages-api.ts)
 *   - agent approvals (local-tasks-package-requests-api.ts, PR3)
 *
 * Sharing the mutex is load-bearing, not just DRY: a concurrent admin-add and
 * agent-approve on the same space would otherwise race the content-addressed
 * overlay build and the `spaces.python_packages` JSON. The persisted desired-
 * state is the source of truth; each mutation rebuilds the whole overlay and
 * atomically repoints `current` (see python-packages.ts).
 */

export interface PersistedPkg {
  name: string;   // PEP 503 normalized
  spec: string;   // canonical pip argv, e.g. "requests==2.32.3"
  addedAt: string;
}
export interface PersistedState {
  packages: PersistedPkg[];
  lockHash?: string;
  updatedAt?: string;
}

export function readState(json: string | null): PersistedState {
  if (!json) return { packages: [] };
  try {
    const v = JSON.parse(json);
    if (v && Array.isArray(v.packages)) return v as PersistedState;
  } catch {
    // fall through
  }
  return { packages: [] };
}

// Per-space in-process mutex so two concurrent installs never race the overlay
// or the read-modify-write of the desired-state JSON. Module-level so it is
// shared across every caller in this process.
const spaceLocks = new Map<string, Promise<unknown>>();
export function withSpaceLock<T>(spaceId: string, fn: () => Promise<T>): Promise<T> {
  const prev = spaceLocks.get(spaceId) ?? Promise.resolve();
  const next = prev.catch(() => {}).then(fn);
  spaceLocks.set(spaceId, next.catch(() => {}));
  return next;
}

export type AddPackageOutcome =
  | { ok: true; state: PersistedState }
  | { conflict: true }
  | { versionConflict: string }  // same name, DIFFERENT spec already pinned (existing spec)
  | { tooMany: true }
  | { installError: string };

/**
 * Add ONE already-validated package to a space's desired-state and rebuild the
 * overlay under the shared per-space lock. Idempotent by name: an existing
 * package returns `{ conflict: true }` without touching the overlay.
 *
 * Callers MUST have validated `parsed` (parseAndValidateSpec + assertNotShadowing)
 * and checked the feature is enabled. This function does NOT re-authorize.
 */
export async function addPackageToSpace(params: {
  repo: Repository;
  cfg: PythonPackagesConfig;
  spaceId: string;
  ownerId: string | null | undefined;
  parsed: ParsedSpec;
  bwrapAvailable: boolean;
}): Promise<AddPackageOutcome> {
  const { repo, cfg, spaceId, ownerId, parsed, bwrapAvailable } = params;
  return withSpaceLock(spaceId, async () => {
    const state = readState(repo.getSpacePythonPackages(spaceId));
    const existing = state.packages.find((p) => p.name === parsed.normalizedName);
    if (existing) {
      // Same exact spec → idempotent no-op success. Same name but a DIFFERENT
      // spec cannot be added (one version per package) — surface it distinctly
      // so the caller doesn't falsely report the requested version as installed.
      return existing.spec === parsed.spec
        ? { conflict: true as const }
        : { versionConflict: existing.spec };
    }
    if (state.packages.length + 1 > cfg.maxPackagesPerSpace) {
      return { tooMany: true as const };
    }
    // Rebuild the FULL set (existing + new) into a fresh content-addressed overlay.
    const specs: ParsedSpec[] = [
      ...state.packages.map((p) => parseAndValidateSpec(p.spec)),
      parsed,
    ];
    const install = await installSpacePackages({
      pkgRoot: cfg.dir,
      spaceKey: spaceKeyFor(spaceId, ownerId),
      specs,
      cfg,
      bwrapAvailable,
    });
    if (!install.ok) return { installError: install.error ?? 'install failed' };
    const next: PersistedState = {
      packages: [
        ...state.packages,
        { name: parsed.normalizedName, spec: parsed.spec, addedAt: new Date().toISOString() },
      ],
      lockHash: install.lockHash,
      updatedAt: new Date().toISOString(),
    };
    repo.setSpacePythonPackages(spaceId, JSON.stringify(next));
    return { ok: true as const, state: next };
  });
}
