// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import fs from "node:fs";
import path from "node:path";
import workflowTemplate from "../../assets/workflows/workflow-template.md" with { type: "text" };
import { resolveAssetPathFromName } from "../../core/asset/asset-spec";
import { isWithin, resolveStashDir } from "../../core/common";
import { UsageError } from "../../core/errors";
import { warn } from "../../core/warn";
import { parseWorkflow } from "../parser";
import type { WorkflowError } from "../schema";

const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate({
  title: "Example Workflow",
  firstStepTitle: "First Step",
  firstStepId: "first-step",
});

export function getWorkflowTemplate(): string {
  return DEFAULT_WORKFLOW_TEMPLATE;
}

export function buildWorkflowTemplate(name?: string): string {
  if (!name) return DEFAULT_WORKFLOW_TEMPLATE;

  const title = humanizeWorkflowName(name);
  const stepId = slugifyWorkflowStepId(name);
  const customized = renderWorkflowTemplate({
    title,
    firstStepTitle: `${title} Setup`,
    firstStepId: `${stepId}-setup`,
  });
  const result = parseWorkflow(customized, { path: `<template:${name}>` });
  if (!result.ok) {
    throw new UsageError(formatWorkflowErrors(`<template:${name}>`, result.errors));
  }
  return customized;
}

export function createWorkflowAsset(input: { name: string; content?: string; from?: string; force?: boolean }): {
  ref: string;
  path: string;
  stashDir: string;
} {
  const stashDir = resolveStashDir();
  const typeRoot = path.join(stashDir, "workflows");
  fs.mkdirSync(typeRoot, { recursive: true });

  const normalizedName = normalizeWorkflowName(input.name);
  const assetPath = resolveAssetPathFromName("workflow", typeRoot, normalizedName);
  if (!isWithin(assetPath, typeRoot)) {
    throw new UsageError(`Resolved workflow path escapes the stash: "${normalizedName}"`, "PATH_ESCAPE_VIOLATION");
  }
  if (fs.existsSync(assetPath) && !input.force) {
    throw new UsageError(
      `Workflow "${normalizedName}" already exists. Re-run with --force to overwrite it.`,
      "RESOURCE_ALREADY_EXISTS",
    );
  }

  const content = input.from
    ? readWorkflowSource(input.from, stashDir)
    : (input.content ?? buildWorkflowTemplate(normalizedName));
  const sourcePath = input.from ?? `workflows/${normalizedName}.md`;
  const result = parseWorkflow(content, { path: sourcePath });
  if (!result.ok) {
    throw new UsageError(formatWorkflowErrors(sourcePath, result.errors));
  }

  fs.mkdirSync(path.dirname(assetPath), { recursive: true });
  fs.writeFileSync(assetPath, content.endsWith("\n") ? content : `${content}\n`, "utf8");

  return {
    ref: `workflow:${normalizedName}`,
    path: assetPath,
    stashDir,
  };
}

function readWorkflowSource(source: string, stashDir: string): string {
  const resolved = path.resolve(source);
  let stat: fs.Stats;
  try {
    stat = fs.statSync(resolved);
  } catch {
    throw new UsageError(`Workflow source not found: "${source}".`);
  }
  if (!stat.isFile()) {
    throw new UsageError(`Workflow source must be a file: "${source}".`);
  }
  // The user is allowed to import any readable file as a workflow body, but
  // an import from outside the stash is unusual enough to warn about. Anyone
  // running `akm workflow create --from /etc/passwd` deserves a heads-up.
  if (!isWithin(resolved, stashDir)) {
    warn(
      `Importing workflow content from outside the stash: ${resolved}\n  ` +
        `If this was unintentional, abort and re-run with a --from path inside ${stashDir}.`,
    );
  }
  return fs.readFileSync(resolved, "utf8");
}

function normalizeWorkflowName(name: string): string {
  const normalized = name
    .trim()
    .replace(/\\/g, "/")
    .replace(/^\/+|\/+$/g, "")
    .replace(/\.md$/i, "");
  if (!normalized) {
    throw new UsageError("Workflow name cannot be empty.");
  }
  const segments = normalized.split("/");
  if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
    throw new UsageError("Workflow name must be a relative path without '.' or '..' segments.");
  }
  return normalized;
}

function humanizeWorkflowName(name: string): string {
  return (
    name
      .split("/")
      .pop()
      ?.replace(/[-_]+/g, " ")
      .replace(/\b\w/g, (match) => match.toUpperCase())
      .trim() || "Example Workflow"
  );
}

function slugifyWorkflowStepId(name: string): string {
  return (
    name
      .split("/")
      .pop()
      ?.toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "") || "workflow"
  );
}

export function formatWorkflowErrors(path: string, errors: WorkflowError[]): string {
  const lines = errors.map((e) => `  ${path}:${e.line} — ${e.message}`);
  const heading = errors.length === 1 ? "Workflow has 1 error:" : `Workflow has ${errors.length} errors:`;
  return [heading, ...lines].join("\n");
}

/**
 * Validate a workflow by ref (`workflow:<name>`) or filesystem path.
 *
 * Returns the parse result plus the source-relative path used. Throws
 * `UsageError` only when the target cannot be located on disk; parse
 * failures are returned as `{ ok: false, errors }` so callers can
 * format them however they like.
 */
export function validateWorkflowSource(target: string): {
  path: string;
  parse: ReturnType<typeof parseWorkflow>;
} {
  if (target.startsWith("workflow:")) {
    throw new UsageError(
      `validateWorkflowSource expects a filesystem path; resolve refs to paths in the caller before invoking.`,
    );
  }
  const resolved = path.resolve(target);
  if (!fs.existsSync(resolved)) {
    throw new UsageError(`Workflow file not found: "${target}".`);
  }
  const content = fs.readFileSync(resolved, "utf8");
  return { path: target, parse: parseWorkflow(content, { path: target }) };
}

function renderWorkflowTemplate(input: { title: string; firstStepTitle: string; firstStepId: string }): string {
  return workflowTemplate
    .replace("{{TITLE}}", input.title)
    .replace("{{FIRST_STEP_TITLE}}", input.firstStepTitle)
    .replace("{{FIRST_STEP_ID}}", input.firstStepId);
}
