import { graphDeploymentLinkSchema, graphDeploymentSyncSchema, intuneDeploymentSchema, psadtMigrationSchema, psadtProfileSchema, psadtValidationSchema } from '../forms/schemas.js';
import { createSyncRun, finishSyncRun, getGraphConnection, listDeploymentStatuses, listDeploymentSyncRuns, listGraphAudit, recordGraphAudit, upsertDeploymentStatuses } from '../models/graphModel.js';
import { createScript, getScript } from '../models/libraryModel.js';
import {
  createIntuneDeployment,
  createPsadtProfile,
  deleteIntuneDeployment,
  deletePsadtProfile,
  getPsadtCatalog,
  listIntuneDeployments,
  listPsadtProfiles,
  loadIntuneDeployment,
  loadPsadtProfile,
  markDeploymentPublished,
  renderPsadtProfile,
  updateIntuneDeployment,
  updatePsadtProfile
} from '../models/psadtModel.js';
import fs from 'node:fs';
import { config } from '../config.js';
import { createChangeRequest, findApprovedRequest, findPendingRequest, markRequestExecuted } from '../models/changeRequestModel.js';
import { buildIntuneWin, builderAvailability } from '../services/intuneWinBuilder.js';
import { assignWin32App, getWin32AppState, setWin32AppRelationships, syncIntuneDeploymentStatus, uploadWin32Content, upsertWin32App } from '../services/graphService.js';
import { buildAssignmentsPayload, buildRelationshipsPayload, buildWin32LobAppPayload } from '../services/intunePublishService.js';
import { computeDrift } from '../services/intuneDriftService.js';
import { canPublish } from '../services/graphRbac.js';
import { cloneForNextVersion, setRingIntent } from '../services/intunePromotionService.js';
import { parseIntunewin } from '../services/intunewinParser.js';
import { createAssetFromUpload, getAssetFile } from '../models/assetModel.js';
import { validatePsadtScript, validationRuleCatalog } from '../services/psadtValidator.js';
import { planPsadtMigration } from '../services/psadtMigrationService.js';
import { basenameAny, path } from '../utils/pathUtils.js';

export function catalog(req, res) {
  res.json(getPsadtCatalog());
}

export function intuneIndex(req, res) {
  res.json(listIntuneDeployments(req.user.id));
}

export function intuneCreate(req, res) {
  const parsed = intuneDeploymentSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid Intune deployment payload' });
  res.status(201).json(createIntuneDeployment(parsed.data, req.user.id));
}

export function intuneShow(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  res.json(deployment);
}

export function intuneUpdate(req, res) {
  const parsed = intuneDeploymentSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid Intune deployment payload' });
  const deployment = updateIntuneDeployment(req.params.id, parsed.data, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  res.json(deployment);
}

export function intuneDestroy(req, res) {
  deleteIntuneDeployment(req.params.id, req.user.id);
  res.status(204).end();
}

export function intuneGraphStatus(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  const statuses = listDeploymentStatuses(deployment.id);
  const syncRuns = listDeploymentSyncRuns(deployment.id);
  res.json({
    deployment,
    summary: summarizeInstallStatuses(statuses),
    failures: statuses.filter((row) => row.installState === 'failed' || row.errorCode),
    statuses,
    syncRuns
  });
}

export function intuneGraphLink(req, res) {
  const parsed = graphDeploymentLinkSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Valid connectionId and graphAppId are required' });
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  const updated = updateIntuneDeployment(req.params.id, {
    ...deployment,
    graphConnectionId: parsed.data.connectionId || deployment.graphConnectionId || null,
    graphAppId: parsed.data.graphAppId
  }, req.user.id);
  res.json(updated);
}

export async function intuneGraphSync(req, res) {
  const parsed = graphDeploymentSyncSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid Graph sync payload' });
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  const connectionId = parsed.data.connectionId || deployment.graphConnectionId;
  const graphAppId = parsed.data.graphAppId || deployment.graphAppId;
  if (!connectionId || !graphAppId) return res.status(400).json({ error: 'Graph connection and Intune mobile app id are required' });
  const connection = getGraphConnection(connectionId, req.user.id, true);
  if (!connection) return res.status(404).json({ error: 'Graph connection not found' });
  const runId = createSyncRun(deployment.id, connectionId, graphAppId);
  try {
    const result = await syncIntuneDeploymentStatus(connection, graphAppId, parsed.data.mode);
    upsertDeploymentStatuses(deployment.id, connectionId, graphAppId, result.rows);
    finishSyncRun(runId, 'success', `Synced ${result.rows.length} status rows from ${result.sources.join(', ') || 'Graph'}`, result.summary);
    const updated = updateIntuneDeployment(req.params.id, {
      ...deployment,
      graphConnectionId: connectionId,
      graphAppId
    }, req.user.id);
    const statuses = listDeploymentStatuses(deployment.id);
    res.json({
      deployment: updated,
      summary: summarizeInstallStatuses(statuses),
      failures: statuses.filter((row) => row.installState === 'failed' || row.errorCode),
      statuses,
      syncRuns: listDeploymentSyncRuns(deployment.id)
    });
  } catch (error) {
    finishSyncRun(runId, 'failed', error.message, {});
    res.status(400).json({ error: error.message, syncRuns: listDeploymentSyncRuns(deployment.id) });
  }
}

// Shared resolver for tenant-changing Graph actions: loads the visible
// deployment and a write-enabled Graph connection (with secret), or sends the
// appropriate error and returns null.
function resolveWriteContext(req, res) {
  const parsed = graphDeploymentSyncSchema.safeParse(req.body || {});
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) { res.status(404).json({ error: 'Intune deployment not found' }); return null; }
  const connectionId = (parsed.success && parsed.data.connectionId) || deployment.graphConnectionId;
  if (!connectionId) { res.status(400).json({ error: 'A Graph connection is required for tenant write-back' }); return null; }
  const connection = getGraphConnection(connectionId, req.user.id, true);
  if (!connection) { res.status(404).json({ error: 'Graph connection not found' }); return null; }
  if (!connection.allow_write) {
    res.status(403).json({ error: 'This Graph connection is read-only. An admin must enable write-back first.' });
    return null;
  }
  if (!canPublish(connection, req.user)) {
    res.status(403).json({ error: 'You are not an authorized publisher for this Graph connection.' });
    return null;
  }
  return { deployment, connection, connectionId };
}

// Governance gate: when a connection requires approval, a tenant-changing action
// only proceeds if an approved (not-yet-executed) change request exists for it.
// Otherwise a pending request is created and the caller is told to wait. Returns
// { proceed, requestId } — when proceed is false a 202 response has been sent.
function ensureApproved(ctx, action, req, res) {
  if (!ctx.connection.require_approval) return { proceed: true, requestId: null };
  const approved = findApprovedRequest(ctx.deployment.id, action);
  if (approved) return { proceed: true, requestId: approved.id };
  const request = findPendingRequest(ctx.deployment.id, action) || createChangeRequest({
    deploymentId: ctx.deployment.id,
    connectionId: ctx.connectionId,
    action,
    requestedBy: req.user.id,
    requesterEmail: req.user.email
  });
  res.status(202).json({ status: 'approval-required', message: 'This action requires approval before it runs.', request });
  return { proceed: false };
}

// Phase 1: publish (create/update) the win32LobApp metadata in the tenant from
// a deployment plan. Every attempt is audit-logged with actor, request, response.
export async function intuneGraphPublish(req, res) {
  const ctx = resolveWriteContext(req, res);
  if (!ctx) return;
  const approval = ensureApproved(ctx, 'publish', req, res);
  if (!approval.proceed) return;
  const { deployment, connection, connectionId } = ctx;
  const profile = deployment.profileId ? loadPsadtProfile(deployment.profileId, req.user.id) : null;
  const { payload, warnings } = buildWin32LobAppPayload(deployment, profile);
  const auditBase = {
    connectionId,
    deploymentId: deployment.id,
    actorUserId: req.user.id,
    actorEmail: req.user.email,
    action: deployment.graphAppId ? 'win32app.update' : 'win32app.create',
    request: payload
  };

  try {
    const result = await upsertWin32App(connection, payload, deployment.graphAppId);
    const updated = updateIntuneDeployment(req.params.id, {
      ...deployment,
      graphConnectionId: connectionId,
      graphAppId: result.id,
      status: 'published'
    }, req.user.id);
    if (approval.requestId) markRequestExecuted(approval.requestId);
    markDeploymentPublished(deployment.id);
    recordGraphAudit({ ...auditBase, targetGraphId: result.id, status: 'success', message: result.updated ? 'Updated win32LobApp' : 'Created win32LobApp', response: result.raw || { id: result.id } });
    res.json({ deployment: updated, graphAppId: result.id, warnings });
  } catch (error) {
    recordGraphAudit({ ...auditBase, status: 'failed', message: error.message, response: error.payload || {} });
    res.status(400).json({ error: error.message, warnings });
  }
}

// Phase 3: replace the app's assignments (rings, filters, exclusions) in tenant.
export async function intuneGraphAssign(req, res) {
  const ctx = resolveWriteContext(req, res);
  if (!ctx) return;
  const { deployment, connection, connectionId } = ctx;
  if (!deployment.graphAppId) return res.status(400).json({ error: 'Publish the app before assigning it.' });
  const approval = ensureApproved(ctx, 'assign', req, res);
  if (!approval.proceed) return;
  const { assignments, warnings } = buildAssignmentsPayload(deployment);
  const auditBase = {
    connectionId, deploymentId: deployment.id, actorUserId: req.user.id, actorEmail: req.user.email,
    action: 'win32app.assign', targetGraphId: deployment.graphAppId, request: { mobileAppAssignments: assignments }
  };
  try {
    const result = await assignWin32App(connection, deployment.graphAppId, assignments);
    if (approval.requestId) markRequestExecuted(approval.requestId);
    recordGraphAudit({ ...auditBase, status: 'success', message: `Assigned ${result.count} target(s)`, response: result });
    res.json({ deployment, assigned: result.count, warnings });
  } catch (error) {
    recordGraphAudit({ ...auditBase, status: 'failed', message: error.message, response: error.payload || {} });
    res.status(400).json({ error: error.message, warnings });
  }
}

// Phase 4: replace the app's supersedence/dependency relationships in tenant.
export async function intuneGraphRelationships(req, res) {
  const ctx = resolveWriteContext(req, res);
  if (!ctx) return;
  const { deployment, connection, connectionId } = ctx;
  if (!deployment.graphAppId) return res.status(400).json({ error: 'Publish the app before setting relationships.' });
  const approval = ensureApproved(ctx, 'relationships', req, res);
  if (!approval.proceed) return;
  let body;
  try {
    body = buildRelationshipsPayload(deployment.relationships, deployment.graphAppId);
  } catch (error) {
    return res.status(400).json({ error: error.message, validation: error.validation || [] });
  }
  const auditBase = {
    connectionId, deploymentId: deployment.id, actorUserId: req.user.id, actorEmail: req.user.email,
    action: 'win32app.relationships', targetGraphId: deployment.graphAppId, request: body
  };
  try {
    const result = await setWin32AppRelationships(connection, deployment.graphAppId, body.relationships);
    if (approval.requestId) markRequestExecuted(approval.requestId);
    recordGraphAudit({ ...auditBase, status: 'success', message: `Set ${result.count} relationship(s)`, response: result });
    res.json({ deployment, relationships: result.count });
  } catch (error) {
    recordGraphAudit({ ...auditBase, status: 'failed', message: error.message, response: error.payload || {} });
    res.status(400).json({ error: error.message });
  }
}

// Phase 2: upload the .intunewin content for a published app. The package is an
// Asset Library file (assetId); we parse it locally for the encrypted payload +
// encryption info, then run the Graph content-version/Azure-block-upload/commit
// flow. NOTE: validated end to end only against a live tenant.
export async function intuneGraphUploadContent(req, res) {
  const ctx = resolveWriteContext(req, res);
  if (!ctx) return;
  const { deployment, connection, connectionId } = ctx;
  if (!deployment.graphAppId) return res.status(400).json({ error: 'Publish the app before uploading content.' });
  const approval = ensureApproved(ctx, 'upload-content', req, res);
  if (!approval.proceed) return;
  const assetId = req.body?.assetId;
  if (!assetId) return res.status(400).json({ error: 'assetId of the .intunewin package is required' });

  const fileRef = getAssetFile(assetId, req.user.id);
  if (!fileRef) return res.status(404).json({ error: 'Asset not found' });

  let parsed;
  try {
    parsed = parseIntunewin(fs.readFileSync(fileRef.filePath));
  } catch (error) {
    return res.status(400).json({ error: `Could not read .intunewin package: ${error.message}` });
  }

  const auditBase = {
    connectionId, deploymentId: deployment.id, actorUserId: req.user.id, actorEmail: req.user.email,
    action: 'win32app.content-upload', targetGraphId: deployment.graphAppId,
    request: { assetId, setupFile: parsed.setupFile, size: parsed.unencryptedContentSize, sizeEncrypted: parsed.encryptedContentSize }
  };
  try {
    const result = await uploadWin32Content(connection, deployment.graphAppId, parsed);
    updateIntuneDeployment(req.params.id, { ...deployment, intunewinFile: fileRef.asset.originalName, status: 'published' }, req.user.id);
    if (approval.requestId) markRequestExecuted(approval.requestId);
    recordGraphAudit({ ...auditBase, status: 'success', message: `Committed content version ${result.contentVersionId}`, response: result });
    res.json({ deployment: loadIntuneDeployment(req.params.id, req.user.id), content: result });
  } catch (error) {
    recordGraphAudit({ ...auditBase, status: 'failed', message: error.message, response: error.payload || {} });
    res.status(400).json({ error: error.message });
  }
}

// Reconcile: re-apply the full plan (metadata, assignments, relationships) to the
// tenant to clear drift. Gated + approval-aware + audited as one action.
export async function intuneGraphReconcile(req, res) {
  const ctx = resolveWriteContext(req, res);
  if (!ctx) return;
  const { deployment, connection, connectionId } = ctx;
  if (!deployment.graphAppId) return res.status(400).json({ error: 'Publish the app before reconciling.' });
  const approval = ensureApproved(ctx, 'reconcile', req, res);
  if (!approval.proceed) return;

  const profile = deployment.profileId ? loadPsadtProfile(deployment.profileId, req.user.id) : null;
  const { payload } = buildWin32LobAppPayload(deployment, profile);
  const { assignments } = buildAssignmentsPayload(deployment);
  let relationshipsBody = { relationships: [] };
  try {
    relationshipsBody = buildRelationshipsPayload(deployment.relationships, deployment.graphAppId);
  } catch { /* invalid relationships are skipped; drift will still flag them */ }

  const auditBase = {
    connectionId, deploymentId: deployment.id, actorUserId: req.user.id, actorEmail: req.user.email,
    action: 'win32app.reconcile', targetGraphId: deployment.graphAppId, request: { app: payload, assignments, relationships: relationshipsBody.relationships }
  };
  try {
    await upsertWin32App(connection, payload, deployment.graphAppId);
    await assignWin32App(connection, deployment.graphAppId, assignments);
    await setWin32AppRelationships(connection, deployment.graphAppId, relationshipsBody.relationships);
    if (approval.requestId) markRequestExecuted(approval.requestId);
    recordGraphAudit({ ...auditBase, status: 'success', message: 'Re-applied plan to tenant' });
    res.json({ deployment, reconciled: true });
  } catch (error) {
    recordGraphAudit({ ...auditBase, status: 'failed', message: error.message, response: error.payload || {} });
    res.status(400).json({ error: error.message });
  }
}

// Report whether this host can build .intunewin packages (Windows + tool, or an
// external packager hook), so the UI can enable/disable the Build action.
export function intuneBuilderStatus(req, res) {
  res.json(builderAvailability());
}

// Build a .intunewin from a source folder using IntuneWinAppUtil (or the
// configured packager) and ingest the result into the Asset Library, linking it
// to the deployment for content upload.
export async function intuneBuild(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  const sourceFolder = req.body?.sourceFolder || deployment.sourceFolder;
  const setupFile = req.body?.setupFile;
  if (!sourceFolder) return res.status(400).json({ error: 'A source folder is required (set it on the deployment or pass sourceFolder).' });
  if (!setupFile) return res.status(400).json({ error: 'A setup file (entry installer relative to the source folder) is required.' });

  const outputDir = path.join(config.toolsDir, '_build', deployment.id);
  try {
    const { outputFile } = await buildIntuneWin({ sourceFolder, setupFile, outputDir });
    const stats = fs.statSync(outputFile);
    const outputName = basenameAny(outputFile, 'package.intunewin');
    const asset = createAssetFromUpload(
      { path: outputFile, originalname: outputName, mimetype: 'application/octet-stream', size: stats.size },
      { name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: outputName },
      req.user.id
    );
    const updated = updateIntuneDeployment(req.params.id, { ...deployment, intunewinFile: asset.originalName }, req.user.id);
    res.status(201).json({ asset, deployment: updated });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
}

// New version: clone a plan into the next version, linked to the same catalog
// application, ready to build/publish. Does not touch the tenant.
export function intuneNewVersion(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  const clone = cloneForNextVersion(deployment, { version: req.body?.version, applicationId: req.body?.applicationId });
  res.status(201).json(createIntuneDeployment(clone, req.user.id));
}

// Promote a rollout ring to a new intent (e.g. Broad available -> required).
// Updates the plan; the operator then re-assigns to push it to the tenant.
export function intunePromote(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  const ring = req.body?.ring;
  const intent = req.body?.intent;
  if (!ring || !['available', 'required', 'uninstall', 'exclude'].includes(intent)) {
    return res.status(400).json({ error: 'A ring and a valid intent (available/required/uninstall/exclude) are required' });
  }
  const { assignments, changed } = setRingIntent(deployment.assignments, ring, intent);
  if (!changed) return res.status(400).json({ error: `No assignment ring named "${ring}" to promote.` });
  const updated = updateIntuneDeployment(req.params.id, { ...deployment, assignments }, req.user.id);
  res.json(updated);
}

// Drift check: compare the stored plan against the live tenant app. Read-only,
// so it needs a Graph connection (with secret) but not the write gate.
export async function intuneGraphDrift(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  if (!deployment.graphAppId) return res.status(400).json({ error: 'This deployment has not been published to a tenant yet.' });
  const connectionId = req.body?.connectionId || req.query?.connectionId || deployment.graphConnectionId;
  if (!connectionId) return res.status(400).json({ error: 'A Graph connection is required to check drift' });
  const connection = getGraphConnection(connectionId, req.user.id, true);
  if (!connection) return res.status(404).json({ error: 'Graph connection not found' });

  const profile = deployment.profileId ? loadPsadtProfile(deployment.profileId, req.user.id) : null;
  const { payload: expectedApp } = buildWin32LobAppPayload(deployment, profile);
  const { assignments: expectedAssignments } = buildAssignmentsPayload(deployment);
  let expectedRelationships = [];
  try {
    expectedRelationships = buildRelationshipsPayload(deployment.relationships, deployment.graphAppId).relationships;
  } catch {
    expectedRelationships = [];
  }

  try {
    const live = await getWin32AppState(connection, deployment.graphAppId);
    const drift = computeDrift({
      expectedApp,
      liveApp: live.app,
      expectedAssignments,
      liveAssignments: live.assignments,
      expectedRelationships,
      liveRelationships: live.relationships
    });
    res.json({ deployment: { id: deployment.id, name: deployment.name, graphAppId: deployment.graphAppId }, ...drift });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
}

export function intuneGraphAudit(req, res) {
  const deployment = loadIntuneDeployment(req.params.id, req.user.id);
  if (!deployment) return res.status(404).json({ error: 'Intune deployment not found' });
  res.json(listGraphAudit({ deploymentId: deployment.id }));
}

export function index(req, res) {
  res.json(listPsadtProfiles(req.user.id));
}

export function create(req, res) {
  const parsed = psadtProfileSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid PSADT profile payload' });
  res.status(201).json(createPsadtProfile(parsed.data, req.user.id));
}

export function show(req, res) {
  const profile = loadPsadtProfile(req.params.id, req.user.id);
  if (!profile) return res.status(404).json({ error: 'PSADT profile not found' });
  res.json(profile);
}

export function update(req, res) {
  const profile = updatePsadtProfile(req.params.id, req.body, req.user.id);
  if (!profile) return res.status(404).json({ error: 'PSADT profile not found' });
  res.json(profile);
}

export function destroy(req, res) {
  deletePsadtProfile(req.params.id, req.user.id);
  res.status(204).end();
}

export function render(req, res) {
  const rendered = renderPsadtProfile(req.params.id, req.user.id);
  if (!rendered) return res.status(404).json({ error: 'PSADT profile not found' });
  res.json(rendered);
}

export function rules(req, res) {
  res.json(validationRuleCatalog());
}

export function validate(req, res) {
  const parsed = psadtValidationSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid PSADT validation payload' });

  let content = parsed.data.content || '';
  const context = { platform: parsed.data.platform };

  if (parsed.data.scriptId) {
    const script = getScript(parsed.data.scriptId, req.user.id);
    if (!script) return res.status(404).json({ error: 'Script not found' });
    content = script.content || '';
    context.scriptId = script.id;
    context.scriptName = script.name;
  }

  if (parsed.data.profileId) {
    const rendered = renderPsadtProfile(parsed.data.profileId, req.user.id);
    if (!rendered) return res.status(404).json({ error: 'PSADT profile not found' });
    content = rendered.script;
    context.profileId = rendered.profile.id;
    context.profileName = rendered.profile.name;
    context.requireAdmin = rendered.profile.requireAdmin;
  }

  res.json(validatePsadtScript(content, context));
}

export function migrationPlan(req, res) {
  const parsed = psadtMigrationSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid PSADT migration payload' });
  const { content, script } = resolveMigrationSource(parsed.data, req.user.id);
  if (parsed.data.scriptId && !script) return res.status(404).json({ error: 'Script not found' });
  res.json({
    sourceScript: script ? { id: script.id, name: script.name } : null,
    ...planPsadtMigration(content, { targetVersion: parsed.data.targetVersion })
  });
}

export function migrationApply(req, res) {
  const parsed = psadtMigrationSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: 'Invalid PSADT migration payload' });
  const { content, script } = resolveMigrationSource(parsed.data, req.user.id);
  if (parsed.data.scriptId && !script) return res.status(404).json({ error: 'Script not found' });
  const plan = planPsadtMigration(content, { targetVersion: parsed.data.targetVersion });
  let migratedScript = null;
  if (parsed.data.createScript) {
    migratedScript = createScript({
      folderId: script?.folderId || null,
      name: `${script?.name || 'Migrated PSADT Script'} - ${parsed.data.nameSuffix}`,
      description: `Migrated to PSADT ${plan.targetVersion}${script ? ` from ${script.name}` : ''}. Review manual migration findings before production deployment.`,
      content: plan.migratedContent,
      visibility: script?.visibility || 'personal',
      groupId: script?.groupId || null
    }, req.user.id);
  }
  res.json({ ...plan, sourceScript: script ? { id: script.id, name: script.name } : null, migratedScript });
}

function resolveMigrationSource(payload, userId) {
  if (payload.scriptId) {
    const script = getScript(payload.scriptId, userId);
    return { script, content: script?.content || '' };
  }
  return { script: null, content: payload.content || '' };
}

function summarizeInstallStatuses(statuses = []) {
  const summary = { total: statuses.length, installed: 0, failed: 0, pending: 0, notInstalled: 0, notApplicable: 0, unknown: 0, devices: 0, users: 0 };
  for (const row of statuses) {
    if (row.scope === 'user') summary.users += 1;
    else summary.devices += 1;
    const key = row.installState || 'unknown';
    if (summary[key] == null) summary.unknown += 1;
    else summary[key] += 1;
  }
  return summary;
}
