const crypto = require('crypto');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { batchUploadCodeEnvFiles } = require('~/server/services/Files/Code/crud');
const {
  getSessionInfo,
  checkIfActive,
  readSandboxFile,
  writeSandboxFile,
} = require('~/server/services/Files/Code/process');
const {
  checkAccess,
  getStorageMetadata,
  resolveRequestTenantId,
  enrichWithSkillConfigurable,
  mergeDeploymentSkillIds,
  createDeploymentSkillMethods,
  isDeploymentSkillFileSource,
  getDeploymentSkillDownloadStream,
} = require('@librechat/api');
const {
  Permissions,
  FileContext,
  ResourceType,
  PermissionBits,
  AccessRoleIds,
  PrincipalType,
  PermissionTypes,
  isEphemeralAgentId,
} = require('librechat-data-provider');
const { checkPermission, grantPermission } = require('~/server/services/PermissionService');
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
const db = require('~/models');

const deploymentSkillMethods = createDeploymentSkillMethods({
  getSkillById: db.getSkillById,
  getSkillByName: db.getSkillByName,
  listSkillsByAccess: db.listSkillsByAccess,
  listAlwaysApplySkills: db.listAlwaysApplySkills,
  listSkillFiles: db.listSkillFiles,
  getSkillFileByPath: db.getSkillFileByPath,
  updateSkillFileContent: db.updateSkillFileContent,
  updateSkillFileCodeEnvIds: db.updateSkillFileCodeEnvIds,
});

function getSkillDbMethods() {
  return deploymentSkillMethods;
}

function withDeploymentSkillIds(ids = []) {
  return mergeDeploymentSkillIds(ids);
}

function getSkillStrategyFunctions(source) {
  if (isDeploymentSkillFileSource(source)) {
    return {
      getDownloadStream: (_req, filepath) => getDeploymentSkillDownloadStream(filepath),
    };
  }
  return getStrategyFunctions(source);
}

function resolveSkillStorage(req, { isImage = false } = {}) {
  const source = getFileStrategy(req.config, { context: FileContext.skill_file, isImage });
  const strategy = getStrategyFunctions(source);
  if (!strategy.saveBuffer) {
    throw new Error(`Storage backend "${source}" does not support file writes`);
  }
  return { saveBuffer: strategy.saveBuffer, source };
}

function basename(relativePath) {
  const slash = relativePath.lastIndexOf('/');
  return slash === -1 ? relativePath : relativePath.slice(slash + 1);
}

async function saveSkillFileContent({ req, skillId, relativePath, content, mimeType }) {
  const existingFile = await db.getSkillFileByPath(skillId, relativePath);
  const tenantId = resolveRequestTenantId(req);
  const fileId = crypto.randomUUID();
  const filename = basename(relativePath);
  const storageFileName = `${fileId}__${filename}`;
  const buffer = Buffer.from(content, 'utf8');
  const storage = resolveSkillStorage(req, { isImage: mimeType.startsWith('image/') });
  const filepath = await storage.saveBuffer({
    userId: req.user.id,
    buffer,
    fileName: storageFileName,
    basePath: 'uploads',
    tenantId,
  });
  const storageMetadata = getStorageMetadata({ filepath, source: storage.source });

  let result;
  try {
    result = await db.upsertSkillFile({
      skillId,
      relativePath,
      file_id: fileId,
      filename,
      filepath,
      ...storageMetadata,
      source: storage.source,
      mimeType,
      bytes: buffer.length,
      isExecutable: false,
      author: req.user._id ?? req.user.id,
      tenantId,
    });
    if (!result) {
      const error = new Error('Skill file save failed to persist metadata');
      error.code = 'SKILL_FILE_UPSERT_NOT_FOUND';
      throw error;
    }
  } catch (error) {
    const { deleteFile } = getStrategyFunctions(storage.source);
    if (deleteFile) {
      await deleteFile(req, { filepath, user: req.user.id, tenantId }).catch(() => undefined);
    }
    throw error;
  }

  if (existingFile && existingFile.filepath !== filepath) {
    const { deleteFile } = getStrategyFunctions(existingFile.source);
    if (deleteFile) {
      deleteFile(req, {
        filepath: existingFile.filepath,
        storageKey: existingFile.storageKey,
        storageRegion: existingFile.storageRegion,
        user: existingFile.author ?? req.user.id,
        tenantId: existingFile.tenantId ?? tenantId,
      }).catch(() => undefined);
    }
  }

  return { bytes: result.bytes, relativePath: result.relativePath };
}

function canCreateSkill({ req }) {
  return checkAccess({
    req,
    user: req.user,
    permissionType: PermissionTypes.SKILLS,
    permissions: [Permissions.USE, Permissions.CREATE],
    getRoleByName: db.getRoleByName,
  });
}

function canEditSkill({ req, skillId }) {
  return checkPermission({
    userId: req.user.id,
    role: req.user.role,
    resourceType: ResourceType.SKILL,
    resourceId: skillId,
    requiredPermission: PermissionBits.EDIT,
  });
}

function isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralSkillsToggle }) {
  if (!skillsCapabilityEnabled) {
    return false;
  }
  if (isEphemeralAgentId(agent.id)) {
    if (agent.skills_enabled === false) {
      return false;
    }
    if (agent.skills_enabled === true) {
      return true;
    }
    return ephemeralSkillsToggle === true;
  }
  return agent.skills_enabled === true;
}

function canAuthorSkillFiles({
  agent,
  scopedEditableSkillIds = [],
  skillCreateAllowed,
  skillsCapabilityEnabled,
  ephemeralSkillsToggle,
}) {
  return (
    isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralSkillsToggle }) &&
    (scopedEditableSkillIds.length > 0 || skillCreateAllowed === true)
  );
}

function grantSkillOwner({ req, skillId }) {
  return grantPermission({
    principalType: PrincipalType.USER,
    principalId: req.user.id,
    resourceType: ResourceType.SKILL,
    resourceId: skillId,
    accessRoleId: AccessRoleIds.SKILL_OWNER,
    grantedBy: req.user.id,
  });
}

function getAuthorSkillByName({ req, name }) {
  const author = req.user?._id ?? req.user?.id;
  if (!author) {
    return null;
  }
  return db.getAuthorSkillByName({
    name,
    author,
    tenantId: resolveRequestTenantId(req),
  });
}

/**
 * Builds the `skillPrimedIdsByName` map threaded through
 * `buildAgentToolContext`. Centralized here so every runtime route shares
 * one source of truth — if `ResolvedManualSkill` ever renames `_id` or
 * gains new identifying fields, only this helper changes.
 *
 * Combines both manual (`$`-popover) primes AND always-apply primes so
 * `read_file` can:
 *  - Relax the `disable-model-invocation: true` gate for either source
 *    (the body is already in context; blocking its own files would be
 *    nonsensical).
 *  - Pin same-name collision lookups to the exact `_id` the resolver
 *    primed (otherwise a newer same-name duplicate could shadow the
 *    body/file pair within a single turn).
 *
 * On the rare overlap (a name appears in both arrays because upstream
 * dedup was skipped), manual wins — manual invocation is explicit user
 * intent and carries the authoritative `_id` for this turn.
 *
 * Returns `undefined` (not `{}`) when both arrays are empty, so the
 * downstream `enrichWithSkillConfigurable` cleanly omits the field from
 * `mergedConfigurable` rather than threading an empty object.
 *
 * @param {Array<{ name: string, _id: { toString(): string } }> | undefined} manualSkillPrimes
 * @param {Array<{ name: string, _id: { toString(): string } }> | undefined} alwaysApplySkillPrimes
 * @returns {Record<string, string> | undefined}
 */
function buildSkillPrimedIdsByName(manualSkillPrimes, alwaysApplySkillPrimes) {
  const manualCount = manualSkillPrimes?.length ?? 0;
  const alwaysApplyCount = alwaysApplySkillPrimes?.length ?? 0;
  if (manualCount === 0 && alwaysApplyCount === 0) {
    return undefined;
  }
  const out = {};
  /* Order matters on the edge case where the same name appears in both
     lists: always-apply goes in first, then manual overwrites — manual
     wins because it's explicit user intent for this turn. */
  if (alwaysApplyCount > 0) {
    for (const p of alwaysApplySkillPrimes) {
      out[p.name] = p._id.toString();
    }
  }
  if (manualCount > 0) {
    for (const p of manualSkillPrimes) {
      out[p.name] = p._id.toString();
    }
  }
  return out;
}

/**
 * Builds the per-agent context consumed by ON_TOOL_EXECUTE. Keeping this
 * shape in one Adapter gives every runtime path the same configurable
 * fields and the same primed-skill pinning behavior.
 *
 * @param {object} params
 * @param {object} params.agent
 * @param {object} params.config
 * @param {Record<string, import('@librechat/api').LCAvailableTools>} [params.config.mcpAvailableTools]
 * @param {import('@librechat/api').RequestScopedMCPConnectionStore} [params.config.requestScopedConnections]
 * @returns {object}
 */
function buildAgentToolContext({ agent, config }) {
  return {
    agent,
    /** Per-agent resolved endpoint token/pricing config. Retained here because
     *  `agentToolContexts` is the one map that holds every agent — including
     *  pure subagents pruned from `agentConfigs` — so usage can be priced with
     *  the producing agent's config in multi-endpoint graphs. */
    endpointTokenConfig: config.endpointTokenConfig,
    toolRegistry: config.toolRegistry,
    mcpAvailableTools: config.mcpAvailableTools,
    requestScopedConnections: config.requestScopedConnections,
    userMCPAuthMap: config.userMCPAuthMap,
    tool_resources: config.tool_resources,
    actionsEnabled: config.actionsEnabled,
    accessibleSkillIds: config.accessibleSkillIds,
    activeSkillNames: config.activeSkillNames,
    codeEnvAvailable: config.codeEnvAvailable,
    skillAuthoringAvailable: config.skillAuthoringAvailable,
    fileAuthoringToolNames: config.fileAuthoringToolNames,
    skillPrimedIdsByName:
      buildSkillPrimedIdsByName(config.manualSkillPrimes, config.alwaysApplySkillPrimes) ?? {},
  };
}

function hasOwn(value, key) {
  return Object.prototype.hasOwnProperty.call(value ?? {}, key);
}

/**
 * Applies per-agent runtime context to a loadToolsForExecution result.
 *
 * @param {object} params
 * @param {{ loadedTools: unknown[], configurable?: Record<string, unknown> }} params.result
 * @param {object} params.req
 * @param {object | undefined} params.ctx
 * @param {object | undefined} [params.fallback]
 * @returns {{ loadedTools: unknown[], configurable: Record<string, unknown> }}
 */
function enrichLoadedToolsWithAgentContext({ result, req, ctx = {}, fallback = {} }) {
  const codeEnvAvailable = hasOwn(ctx, 'codeEnvAvailable')
    ? ctx.codeEnvAvailable === true
    : fallback.codeEnvAvailable === true;
  const skillAuthoringAvailable = hasOwn(ctx, 'skillAuthoringAvailable')
    ? ctx.skillAuthoringAvailable === true
    : fallback.skillAuthoringAvailable === true;

  return enrichWithSkillConfigurable({
    result,
    context: {
      req,
      codeEnvAvailable,
      accessibleSkillIds: ctx.accessibleSkillIds ?? fallback.accessibleSkillIds,
      skillPrimedIdsByName: ctx.skillPrimedIdsByName ?? fallback.skillPrimedIdsByName,
      activeSkillNames: ctx.activeSkillNames ?? fallback.activeSkillNames,
      skillAuthoringAvailable,
      fileAuthoringToolNames: ctx.fileAuthoringToolNames ?? fallback.fileAuthoringToolNames,
    },
  });
}

/** Skill-related properties for ToolExecuteOptions (stable references, allocated once). */
const skillToolDeps = {
  getSkillByName: deploymentSkillMethods.getSkillByName,
  getAuthorSkillByName,
  createSkill: db.createSkill,
  updateSkill: db.updateSkill,
  deleteSkill: db.deleteSkill,
  canCreateSkill,
  canEditSkill,
  grantSkillOwner,
  saveSkillFileContent,
  listSkillFiles: deploymentSkillMethods.listSkillFiles,
  getStrategyFunctions: getSkillStrategyFunctions,
  batchUploadCodeEnvFiles,
  getSessionInfo,
  checkIfActive,
  updateSkillFileCodeEnvIds: deploymentSkillMethods.updateSkillFileCodeEnvIds,
  getSkillFileByPath: deploymentSkillMethods.getSkillFileByPath,
  updateSkillFileContent: deploymentSkillMethods.updateSkillFileContent,
  /**
   * `read_file` falls back to a sandbox `cat` for `/mnt/data/...` paths
   * and for `{firstSegment}/...` paths whose first segment isn't a known
   * skill name. The handler routes through this when the agent has code
   * execution enabled; the codeapi base URL comes from
   * `LIBRECHAT_CODE_BASEURL` and the sandbox session id is forwarded by
   * the agents-side `ToolNode` via `tc.codeSessionContext`.
   */
  readSandboxFile,
  writeSandboxFile,
};

function getSkillToolDeps() {
  return skillToolDeps;
}

module.exports = {
  getSkillToolDeps,
  canAuthorSkillFiles,
  isAgentSkillsEnabledForRun,
  getSkillDbMethods,
  withDeploymentSkillIds,
  getSkillStrategyFunctions,
  enrichWithSkillConfigurable,
  buildSkillPrimedIdsByName,
  buildAgentToolContext,
  enrichLoadedToolsWithAgentContext,
};
