// Zod schema definition for *.agent.yaml files
const assert = require('node:assert');
const { z } = require('zod');

const COMMAND_TARGET_KEYS = ['workflow', 'validate-workflow', 'exec', 'action', 'tmpl', 'data'];
const TRIGGER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

// Public API ---------------------------------------------------------------

/**
 * Validate an agent YAML payload against the schema derived from its file location.
 * Exposed as the single public entry point, so callers do not reach into schema internals.
 *
 * @param {string} filePath Path to the agent file (used to infer module scope).
 * @param {unknown} agentYaml Parsed YAML content.
 * @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
 */
function validateAgentFile(filePath, agentYaml) {
  const expectedModule = deriveModuleFromPath(filePath);
  const schema = agentSchema({ module: expectedModule });
  return schema.safeParse(agentYaml);
}

module.exports = { validateAgentFile };

// Internal helpers ---------------------------------------------------------

/**
 * Build a Zod schema for validating a single agent definition.
 * The schema is generated per call so module-scoped agents can pass their expected
 * module slug while core agents leave it undefined.
 *
 * @param {Object} [options]
 * @param {string|null|undefined} [options.module] Module slug for module agents; omit or null for core agents.
 * @returns {import('zod').ZodSchema} Configured Zod schema instance.
 */
function agentSchema(options = {}) {
  const expectedModule = normalizeModuleOption(options.module);

  return (
    z
      .object({
        agent: buildAgentSchema(expectedModule),
      })
      .strict()
      // Refinement: enforce trigger format and uniqueness rules after structural checks.
      .superRefine((value, ctx) => {
        const seenTriggers = new Set();

        let index = 0;
        for (const item of value.agent.menu) {
          // Handle legacy format with trigger field
          if (item.trigger) {
            const triggerValue = item.trigger;

            if (!TRIGGER_PATTERN.test(triggerValue)) {
              ctx.addIssue({
                code: 'custom',
                path: ['agent', 'menu', index, 'trigger'],
                message: 'agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)',
              });
              return;
            }

            if (seenTriggers.has(triggerValue)) {
              ctx.addIssue({
                code: 'custom',
                path: ['agent', 'menu', index, 'trigger'],
                message: `agent.menu[].trigger duplicates "${triggerValue}" within the same agent`,
              });
              return;
            }

            seenTriggers.add(triggerValue);
          }
          // Handle multi format with triggers array (new format)
          else if (item.triggers && Array.isArray(item.triggers)) {
            for (const [triggerIndex, triggerItem] of item.triggers.entries()) {
              let triggerName = null;

              // Extract trigger name from all three formats
              if (triggerItem.trigger) {
                // Format 1: Simple flat format with trigger field
                triggerName = triggerItem.trigger;
              } else {
                // Format 2a or 2b: Object-key format
                const keys = Object.keys(triggerItem);
                if (keys.length === 1 && keys[0] !== 'trigger') {
                  triggerName = keys[0];
                }
              }

              if (triggerName) {
                if (!TRIGGER_PATTERN.test(triggerName)) {
                  ctx.addIssue({
                    code: 'custom',
                    path: ['agent', 'menu', index, 'triggers', triggerIndex],
                    message: `agent.menu[].triggers[] must be kebab-case (lowercase words separated by hyphen) - got "${triggerName}"`,
                  });
                  return;
                }

                if (seenTriggers.has(triggerName)) {
                  ctx.addIssue({
                    code: 'custom',
                    path: ['agent', 'menu', index, 'triggers', triggerIndex],
                    message: `agent.menu[].triggers[] duplicates "${triggerName}" within the same agent`,
                  });
                  return;
                }

                seenTriggers.add(triggerName);
              }
            }
          }

          index += 1;
        }
      })
      // Refinement: suggest conversational_knowledge when discussion is true
      .superRefine((value, ctx) => {
        if (value.agent.discussion === true && !value.agent.conversational_knowledge) {
          ctx.addIssue({
            code: 'custom',
            path: ['agent', 'conversational_knowledge'],
            message: 'It is recommended to include conversational_knowledge when discussion is true',
          });
        }
      })
  );
}

/**
 * Assemble the full agent schema using the module expectation provided by the caller.
 * @param {string|null} expectedModule Trimmed module slug or null for core agents.
 */
function buildAgentSchema(expectedModule) {
  return z
    .object({
      metadata: buildMetadataSchema(expectedModule),
      persona: buildPersonaSchema(),
      critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(),
      menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }),
      prompts: z.array(buildPromptSchema()).optional(),
      webskip: z.boolean().optional(),
      discussion: z.boolean().optional(),
      conversational_knowledge: z.array(z.object({}).passthrough()).min(1).optional(),
    })
    .strict();
}

/**
 * Validate metadata shape and cross-check module expectation against caller input.
 * @param {string|null} expectedModule Trimmed module slug or null when core agent metadata is expected.
 */
function buildMetadataSchema(expectedModule) {
  const schemaShape = {
    id: createNonEmptyString('agent.metadata.id'),
    name: createNonEmptyString('agent.metadata.name'),
    title: createNonEmptyString('agent.metadata.title'),
    icon: createNonEmptyString('agent.metadata.icon'),
    module: createNonEmptyString('agent.metadata.module').optional(),
  };

  return (
    z
      .object(schemaShape)
      .strict()
      // Refinement: guard presence and correctness of metadata.module.
      .superRefine((value, ctx) => {
        const moduleValue = typeof value.module === 'string' ? value.module.trim() : null;

        if (expectedModule && !moduleValue) {
          ctx.addIssue({
            code: 'custom',
            path: ['module'],
            message: 'module-scoped agents must declare agent.metadata.module',
          });
        } else if (!expectedModule && moduleValue) {
          ctx.addIssue({
            code: 'custom',
            path: ['module'],
            message: 'core agents must not include agent.metadata.module',
          });
        } else if (expectedModule && moduleValue !== expectedModule) {
          ctx.addIssue({
            code: 'custom',
            path: ['module'],
            message: `agent.metadata.module must equal "${expectedModule}"`,
          });
        }
      })
  );
}

function buildPersonaSchema() {
  return z
    .object({
      role: createNonEmptyString('agent.persona.role'),
      identity: createNonEmptyString('agent.persona.identity'),
      communication_style: createNonEmptyString('agent.persona.communication_style'),
      principles: z.union([
        createNonEmptyString('agent.persona.principles'),
        z
          .array(createNonEmptyString('agent.persona.principles[]'))
          .min(1, { message: 'agent.persona.principles must include at least one entry' }),
      ]),
    })
    .strict();
}

function buildPromptSchema() {
  return z
    .object({
      id: createNonEmptyString('agent.prompts[].id'),
      content: z.string().refine((value) => value.trim().length > 0, {
        message: 'agent.prompts[].content must be a non-empty string',
      }),
      description: createNonEmptyString('agent.prompts[].description').optional(),
    })
    .strict();
}

/**
 * Schema for individual menu entries ensuring they are actionable.
 * Supports both legacy format and new multi format.
 */
function buildMenuItemSchema() {
  // Legacy menu item format
  const legacyMenuItemSchema = z
    .object({
      trigger: createNonEmptyString('agent.menu[].trigger'),
      description: createNonEmptyString('agent.menu[].description'),
      workflow: createNonEmptyString('agent.menu[].workflow').optional(),
      'workflow-install': createNonEmptyString('agent.menu[].workflow-install').optional(),
      'validate-workflow': createNonEmptyString('agent.menu[].validate-workflow').optional(),
      exec: createNonEmptyString('agent.menu[].exec').optional(),
      action: createNonEmptyString('agent.menu[].action').optional(),
      tmpl: createNonEmptyString('agent.menu[].tmpl').optional(),
      data: z.string().optional(),
      checklist: createNonEmptyString('agent.menu[].checklist').optional(),
      document: createNonEmptyString('agent.menu[].document').optional(),
      'ide-only': z.boolean().optional(),
      'web-only': z.boolean().optional(),
      discussion: z.boolean().optional(),
    })
    .strict()
    .superRefine((value, ctx) => {
      const hasCommandTarget = COMMAND_TARGET_KEYS.some((key) => {
        const commandValue = value[key];
        return typeof commandValue === 'string' && commandValue.trim().length > 0;
      });

      if (!hasCommandTarget) {
        ctx.addIssue({
          code: 'custom',
          message: 'agent.menu[] entries must include at least one command target field',
        });
      }
    });

  // Multi menu item format
  const multiMenuItemSchema = z
    .object({
      multi: createNonEmptyString('agent.menu[].multi'),
      triggers: z
        .array(
          z.union([
            // Format 1: Simple flat format (has trigger field)
            z
              .object({
                trigger: z.string(),
                input: createNonEmptyString('agent.menu[].triggers[].input'),
                route: createNonEmptyString('agent.menu[].triggers[].route').optional(),
                action: createNonEmptyString('agent.menu[].triggers[].action').optional(),
                data: z.string().optional(),
                type: z.enum(['exec', 'action', 'workflow']).optional(),
              })
              .strict()
              .refine((data) => data.trigger, { message: 'Must have trigger field' })
              .superRefine((value, ctx) => {
                // Must have either route or action (or both)
                if (!value.route && !value.action) {
                  ctx.addIssue({
                    code: 'custom',
                    message: 'agent.menu[].triggers[] must have either route or action (or both)',
                  });
                }
              }),
            // Format 2a: Object with array format (like bmad-builder.agent.yaml)
            z
              .object({})
              .passthrough()
              .refine(
                (value) => {
                  const keys = Object.keys(value);
                  if (keys.length !== 1) return false;
                  const triggerItems = value[keys[0]];
                  return Array.isArray(triggerItems);
                },
                { message: 'Must be object with single key pointing to array' },
              )
              .superRefine((value, ctx) => {
                const triggerName = Object.keys(value)[0];
                const triggerItems = value[triggerName];

                if (!Array.isArray(triggerItems)) {
                  ctx.addIssue({
                    code: 'custom',
                    message: `Trigger "${triggerName}" must be an array of items`,
                  });
                  return;
                }

                // Check required fields in the array
                const hasInput = triggerItems.some((item) => 'input' in item);
                const hasRouteOrAction = triggerItems.some((item) => 'route' in item || 'action' in item);

                if (!hasInput) {
                  ctx.addIssue({
                    code: 'custom',
                    message: `Trigger "${triggerName}" must have an input field`,
                  });
                }

                if (!hasRouteOrAction) {
                  ctx.addIssue({
                    code: 'custom',
                    message: `Trigger "${triggerName}" must have a route or action field`,
                  });
                }
              }),
            // Format 2b: Object with direct fields (like analyst.agent.yaml)
            z
              .object({})
              .passthrough()
              .refine(
                (value) => {
                  const keys = Object.keys(value);
                  if (keys.length !== 1) return false;
                  const triggerFields = value[keys[0]];
                  return !Array.isArray(triggerFields) && typeof triggerFields === 'object';
                },
                { message: 'Must be object with single key pointing to object' },
              )
              .superRefine((value, ctx) => {
                const triggerName = Object.keys(value)[0];
                const triggerFields = value[triggerName];

                // Check required fields
                if (!triggerFields.input || typeof triggerFields.input !== 'string') {
                  ctx.addIssue({
                    code: 'custom',
                    message: `Trigger "${triggerName}" must have an input field`,
                  });
                }

                if (!triggerFields.route && !triggerFields.action) {
                  ctx.addIssue({
                    code: 'custom',
                    message: `Trigger "${triggerName}" must have a route or action field`,
                  });
                }
              }),
          ]),
        )
        .min(1, { message: 'agent.menu[].triggers must have at least one trigger' }),
      discussion: z.boolean().optional(),
    })
    .strict()
    .superRefine((value, ctx) => {
      // Check for duplicate trigger names
      const seenTriggers = new Set();
      for (const [index, triggerItem] of value.triggers.entries()) {
        let triggerName = null;

        // Extract trigger name from either format
        if (triggerItem.trigger) {
          // Format 1
          triggerName = triggerItem.trigger;
        } else {
          // Format 2
          const keys = Object.keys(triggerItem);
          if (keys.length === 1) {
            triggerName = keys[0];
          }
        }

        if (triggerName) {
          if (seenTriggers.has(triggerName)) {
            ctx.addIssue({
              code: 'custom',
              path: ['agent', 'menu', 'triggers', index],
              message: `Trigger name "${triggerName}" is duplicated`,
            });
          }
          seenTriggers.add(triggerName);

          // Validate trigger name format
          if (!TRIGGER_PATTERN.test(triggerName)) {
            ctx.addIssue({
              code: 'custom',
              path: ['agent', 'menu', 'triggers', index],
              message: `Trigger name "${triggerName}" must be kebab-case (lowercase words separated by hyphen)`,
            });
          }
        }
      }
    });

  return z.union([legacyMenuItemSchema, multiMenuItemSchema]);
}

/**
 * Derive the expected module slug from a file path residing under src/modules/<module>/agents/.
 * @param {string} filePath Absolute or relative agent path.
 * @returns {string|null} Module slug if identifiable, otherwise null.
 */
function deriveModuleFromPath(filePath) {
  assert(filePath, 'validateAgentFile expects filePath to be provided');
  assert(typeof filePath === 'string', 'validateAgentFile expects filePath to be a string');
  assert(filePath.startsWith('src/'), 'validateAgentFile expects filePath to start with "src/"');

  const marker = 'src/modules/';
  if (!filePath.startsWith(marker)) {
    return null;
  }

  const remainder = filePath.slice(marker.length);
  const slashIndex = remainder.indexOf('/');
  return slashIndex === -1 ? null : remainder.slice(0, slashIndex);
}

function normalizeModuleOption(moduleOption) {
  if (typeof moduleOption !== 'string') {
    return null;
  }

  const trimmed = moduleOption.trim();
  return trimmed.length > 0 ? trimmed : null;
}

// Primitive validators -----------------------------------------------------

function createNonEmptyString(label) {
  return z.string().refine((value) => value.trim().length > 0, {
    message: `${label} must be a non-empty string`,
  });
}
