import { getMetadata, isClass, type Type } from '@frontmcp/di';
import { z } from '@frontmcp/lazy-zod';
import { entryAvailabilitySchema, isValidMcpUri, type EntryAvailability } from '@frontmcp/utils';

import { InvalidInputError } from '../../errors/mcp.error';
import { type ToolContext } from '../interfaces';
import { FrontMcpToolTokens } from '../tokens';
import { type RawZodShape } from '../types';

// ============================================
// Skill Metadata Types
// ============================================

declare global {
  /**
   * Declarative metadata extends to the Skill decorator.
   */
  interface ExtendFrontMcpSkillMetadata {}
}

/**
 * Bundled resource directories for a skill.
 * Maps to scripts/, references/, and assets/ directories per Agent Skills spec.
 */
export interface SkillResources {
  /** Path to scripts directory */
  scripts?: string;
  /** Path to references directory */
  references?: string;
  /** Path to assets directory */
  assets?: string;
  /** Path to examples directory */
  examples?: string;
}

/**
 * Reference to a tool used by a skill.
 * Can be a simple string (tool name), a tool class, or a detailed reference with purpose.
 */
export interface SkillToolRef {
  /**
   * The name of the tool being referenced.
   */
  name: string;

  /**
   * Optional description of why/how this tool is used in the skill.
   * Helps LLMs understand the tool's role in the workflow.
   */
  purpose?: string;

  /**
   * Whether this tool is required for the skill to function.
   * If true and the tool is missing, skill execution may fail.
   * Default: false
   */
  required?: boolean;
}

/**
 * Input type for tool references in skill metadata.
 * Supports:
 * - Tool class (recommended): Automatically extracts tool name from decorated class
 * - String: Tool name directly (useful for dynamic/external tools)
 * - SkillToolRef: Detailed reference with purpose and required flag
 */
export type SkillToolInput = string | Type<ToolContext> | SkillToolRef | SkillToolRefWithClass;

/**
 * Detailed tool reference that includes a class for automatic name extraction.
 */
export interface SkillToolRefWithClass {
  /**
   * The tool class decorated with @Tool.
   * The tool name will be extracted automatically.
   */
  tool: Type<ToolContext>;

  /**
   * Optional description of why/how this tool is used in the skill.
   */
  purpose?: string;

  /**
   * Whether this tool is required for the skill to function.
   */
  required?: boolean;
}

/**
 * A reference to an OpenAPI operation that a skill is allowed to call from
 * its agentscript. Populated by the deploy pipeline's markdown harvester
 * (see `@frontmcp/adapters/skills` → `extractOpReferences`), which scans
 * `op://` and `[[op:...]]` mentions across SKILL.md / references/ / examples/
 * and produces the deduplicated set per skill.
 *
 * A skill is "executable" if it has any `referencedOperations` OR any
 * decorator-declared `tools`. A skill with neither is "knowledge-only" — it
 * appears in `searchKnowledge` results but its `execute()` calls have no
 * tool bindings (pure computation only).
 */
export interface SkillReferencedOperation {
  /** The OpenAPI spec id (matches the deploy manifest's specs[].id). */
  spec: string;
  /** The OpenAPI operationId — must be a valid JavaScript identifier. */
  operationId: string;
}

/**
 * Parameter definition for a skill.
 * Parameters are inputs that customize skill behavior.
 */
export interface SkillParameter {
  /**
   * Parameter name (identifier).
   */
  name: string;

  /**
   * Human-readable description of the parameter.
   */
  description?: string;

  /**
   * Whether this parameter is required.
   * Default: false
   */
  required?: boolean;

  /**
   * Type hint for the parameter value.
   * Default: 'string'
   */
  type?: 'string' | 'number' | 'boolean' | 'object' | 'array';

  /**
   * Default value for the parameter.
   */
  default?: unknown;
}

/**
 * Example usage scenario for a skill.
 * Helps LLMs understand when and how to use the skill.
 */
export interface SkillExample {
  /**
   * Description of the scenario where this skill applies.
   */
  scenario: string;

  /**
   * Optional parameter values for this example.
   */
  parameters?: Record<string, unknown>;

  /**
   * Description of the expected outcome when the skill completes.
   */
  expectedOutcome?: string;
}

/**
 * Instruction source for a skill.
 * Instructions can be provided inline, from a file, or from a URL.
 */
export type SkillInstructionSource =
  | string // Inline instructions
  | { file: string } // File path to instructions
  | { url: string }; // URL to fetch instructions

/**
 * Declarative metadata describing what a Skill provides.
 * Skills are modular knowledge/workflow packages that teach AI how to perform multi-step tasks.
 */
export interface SkillMetadata extends ExtendFrontMcpSkillMetadata {
  /**
   * Optional unique identifier for the skill.
   * If omitted, the name will be used as the identifier.
   */
  id?: string;

  /**
   * Unique name for the skill.
   * Used for discovery and invocation.
   */
  name: string;

  /**
   * Short description of what the skill does.
   * Used in search results and discovery.
   */
  description: string;

  /**
   * Detailed instructions for performing the skill.
   * Can be an inline string, file path, or URL.
   *
   * @example Inline instructions
   * ```typescript
   * instructions: 'Step 1: Review the PR...\nStep 2: Check for issues...'
   * ```
   *
   * @example File-based instructions
   * ```typescript
   * instructions: { file: './skills/review-pr.md' }
   * ```
   *
   * @example URL-based instructions
   * ```typescript
   * instructions: { url: 'https://example.com/skills/review-pr.md' }
   * ```
   */
  instructions: SkillInstructionSource;

  /**
   * Tools that this skill uses or depends on.
   * Can be tool classes (recommended), tool names, or detailed references.
   *
   * @example Using tool classes (recommended)
   * ```typescript
   * tools: [GitHubGetPRTool, GitHubAddCommentTool]
   * ```
   *
   * @example Using tool classes with purpose
   * ```typescript
   * tools: [
   *   { tool: GitHubGetPRTool, purpose: 'Fetch PR details', required: true },
   *   { tool: GitHubAddCommentTool, purpose: 'Add review comments' },
   * ]
   * ```
   *
   * @example Using string names (for dynamic/external tools)
   * ```typescript
   * tools: ['github_create_pr', 'github_add_comment']
   * ```
   *
   * @example Mixed references
   * ```typescript
   * tools: [
   *   GitHubGetPRTool,  // Class - name auto-extracted
   *   { tool: GitHubAddCommentTool, purpose: 'Add comments' },
   *   'external_api_tool',  // String - for dynamic tools
   *   { name: 'legacy_tool', purpose: 'Legacy integration', required: true },
   * ]
   * ```
   */
  tools?: SkillToolInput[];

  /**
   * Tags for categorization and discovery.
   * Used to filter and search for skills.
   */
  tags?: string[];

  /**
   * OpenAPI operations the skill is allowed to call from its agentscript.
   * Populated by the deploy pipeline's markdown harvester. Read-only at
   * runtime — to change the allowed set, edit the skill's markdown and
   * redeploy.
   */
  referencedOperations?: SkillReferencedOperation[];

  /**
   * Input parameters that customize skill behavior.
   */
  parameters?: SkillParameter[];

  /**
   * Usage examples demonstrating when to use this skill.
   */
  examples?: SkillExample[];

  /**
   * Priority weight for search ranking.
   * Higher values appear earlier in search results.
   * Default: 0
   */
  priority?: number;

  /**
   * If true, the skill will not be shown in discovery/search results.
   * Can still be loaded directly by ID/name.
   * Use case: internal skills not meant for general discovery.
   * Default: false
   */
  hideFromDiscovery?: boolean;

  /**
   * If true, this skill is treated as always loaded — its bindings (operations
   * and namespaces derived from referenced openapi operations) are merged into
   * every `codecall:execute` invocation regardless of the skills list the
   * agent passes. Use sparingly for "standard library" capabilities (logging
   * helpers, auth helpers, organization-wide utilities) that every agentscript
   * is expected to reach for.
   *
   * Always-loaded skills still appear in discovery; clients can suppress them
   * from search results via the `excludeAlwaysLoaded` option.
   *
   * Default: false
   */
  alwaysLoad?: boolean;

  /**
   * Validation mode for tool references.
   * Controls what happens when the skill references tools that are missing or hidden.
   *
   * - 'strict': Fail initialization if any referenced tools are missing/hidden
   * - 'warn': Log warnings but continue initialization (default)
   * - 'ignore': Skip tool validation entirely
   *
   * @default 'warn'
   *
   * @example Strict validation (fail on missing tools)
   * ```typescript
   * @Skill({
   *   name: 'review-pr',
   *   tools: ['github_get_pr', 'github_add_comment'],
   *   toolValidation: 'strict',  // Fail if tools missing
   * })
   * class ReviewPRSkill {}
   * ```
   */
  toolValidation?: 'strict' | 'warn' | 'ignore';

  /**
   * Where this skill is visible for discovery.
   * Controls which discovery mechanisms can find this skill.
   *
   * - 'mcp': Only via skill:// MCP resources (skill://index.json, skill://{skillPath}/SKILL.md)
   * - 'http': Only via HTTP API endpoints (/llm.txt, /skills)
   * - 'both': Visible in both MCP and HTTP (default)
   *
   * Note: hideFromDiscovery=true hides from search but skill is still loadable by ID.
   *
   * @default 'both'
   *
   * @example HTTP-only skill (not visible via skill://index.json MCP resource)
   * ```typescript
   * @Skill({
   *   name: 'internal-process',
   *   visibility: 'http',
   *   instructions: { file: './internal.md' },
   * })
   * class InternalProcessSkill {}
   * ```
   */
  visibility?: 'mcp' | 'http' | 'both';

  /**
   * License name or reference to a bundled LICENSE file.
   * Per Agent Skills specification.
   *
   * @example 'MIT'
   * @example 'Apache-2.0'
   */
  license?: string;

  /**
   * Environment requirements or compatibility notes (max 500 chars).
   * Per Agent Skills specification.
   *
   * @example 'Requires Node.js 18+ and git CLI'
   */
  compatibility?: string;

  /**
   * Arbitrary key-value metadata map.
   * Maps to the `metadata` field in the Anthropic Agent Skills specification.
   * Named `specMetadata` to avoid conflict with the internal `metadata` token.
   */
  specMetadata?: Record<string, string>;

  /**
   * Space-delimited list of pre-approved tool names.
   * Maps to the `allowed-tools` field in the Anthropic Agent Skills specification.
   * Tools listed here are considered pre-approved for the skill and don't require
   * additional user confirmation.
   *
   * @example 'Read Edit Bash(git status) Bash(git diff)'
   */
  allowedTools?: string;

  /**
   * Bundled resource directories (scripts/, references/, assets/).
   * Per Agent Skills specification.
   */
  resources?: SkillResources;

  /**
   * Environment availability constraint.
   * When set, the skill is only discoverable and loadable in matching environments.
   *
   * @example macOS only skill
   * ```typescript
   * @Skill({ name: 'xcode-review', availableWhen: { platform: ['darwin'] } })
   * ```
   */
  availableWhen?: EntryAvailability;

  /**
   * Optional skill quality rating (0..5, one decimal). Surfaced via the
   * Skills HTTP API for consumers that want to filter by `min-rating` or
   * sort by quality. Aligns with Glama / Smithery marketplace conventions.
   *
   * Hosts may populate this from a SKILL.md frontmatter field, a SaaS
   * source, or by overlaying their own quality scoring.
   */
  rating?: number;

  /**
   * Multi-segment URI path under the SEP-2640 `skill://` scheme.
   *
   * Per SEP-2640 §Resource Mapping the URI is `skill://<skill-path>/SKILL.md`
   * where `<skill-path>` may be a single segment (`git-workflow`) or nested
   * (`acme/billing/refunds`). The FINAL segment MUST equal the skill's
   * `name`; preceding segments are an organisational prefix chosen by the
   * server.
   *
   * When omitted, the skill is exposed at `skill://<name>/SKILL.md` (flat).
   *
   * @example Flat
   * ```typescript
   * { name: 'git-workflow' } // -> skill://git-workflow/SKILL.md
   * ```
   *
   * @example Nested
   * ```typescript
   * { name: 'refunds', skillPath: ['acme', 'billing', 'refunds'] }
   * // -> skill://acme/billing/refunds/SKILL.md
   * ```
   */
  skillPath?: string[];

  /**
   * Optional skill category for grouping in discovery (e.g. "deployment",
   * "testing"). The static catalog already organizes skills into category
   * directories; this field surfaces that for HTTP API filtering and for
   * dynamically registered skills that don't sit under the catalog tree.
   */
  category?: string;
}

// ============================================
// Zod Schemas for Validation
// ============================================

const skillToolRefSchema = z.object({
  name: z.string().min(1),
  purpose: z.string().optional(),
  required: z.boolean().optional().default(false),
});

const skillToolRefWithClassSchema = z.object({
  tool: z.function(), // Tool class
  purpose: z.string().optional(),
  required: z.boolean().optional().default(false),
});

// Accepts: string, tool class (function), SkillToolRef, or SkillToolRefWithClass
const skillToolInputSchema = z.union([
  z.string().min(1), // String tool name
  z.function(), // Tool class (will be validated at runtime)
  skillToolRefSchema, // { name, purpose?, required? }
  skillToolRefWithClassSchema, // { tool, purpose?, required? }
]);

const skillReferencedOperationSchema = z
  .object({
    spec: z
      .string()
      .min(1)
      .max(128)
      .regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/, {
        message: 'spec must be a URI-safe identifier',
      }),
    operationId: z
      .string()
      .min(1)
      .max(128)
      .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, {
        message: 'operationId must be a valid JavaScript identifier',
      }),
  })
  .strict();

const skillParameterSchema = z.object({
  name: z.string().min(1),
  description: z.string().optional(),
  required: z.boolean().optional().default(false),
  type: z.enum(['string', 'number', 'boolean', 'object', 'array']).optional().default('string'),
  default: z.unknown().optional(),
});

const skillExampleSchema = z.object({
  scenario: z.string().min(1),
  parameters: z.record(z.string(), z.unknown()).optional(),
  expectedOutcome: z.string().optional(),
});

const skillInstructionSourceSchema = z.union([
  z.string().min(1), // Inline instructions
  z.object({ file: z.string().min(1) }).strict(), // File path
  z
    .object({
      url: z.string().refine(isValidMcpUri, {
        message: 'URL must have a valid scheme (e.g., https://, file://, custom://)',
      }),
    })
    .strict(), // URL
]);

/**
 * Validation mode for skill tool references.
 */
export type SkillToolValidationMode = 'strict' | 'warn' | 'ignore';

/**
 * Zod schema for validating SkillMetadata.
 */
/**
 * Visibility mode for skill discovery.
 * Controls which mechanisms can find this skill.
 */
export type SkillVisibility = 'mcp' | 'http' | 'both';

const skillResourcesSchema = z.object({
  scripts: z.string().optional(),
  references: z.string().optional(),
  assets: z.string().optional(),
  examples: z.string().optional(),
});

export const skillMetadataSchema = z
  .object({
    id: z.string().optional(),
    name: z
      .string()
      .min(1)
      .max(64)
      .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
        message:
          'Skill name must be kebab-case: lowercase letters, numbers, and hyphens only. Must not start/end with a hyphen.',
      })
      .refine((n) => !n.includes('--'), {
        message: 'Skill name must not contain consecutive hyphens.',
      }),
    description: z.string().min(1).max(1024),
    instructions: skillInstructionSourceSchema,
    tools: z.array(skillToolInputSchema).optional(),
    tags: z.array(z.string().min(1)).optional(),
    referencedOperations: z.array(skillReferencedOperationSchema).max(256).optional(),
    parameters: z.array(skillParameterSchema).optional(),
    examples: z.array(skillExampleSchema).optional(),
    priority: z.number().optional().default(0),
    hideFromDiscovery: z.boolean().optional().default(false),
    alwaysLoad: z.boolean().optional().default(false),
    toolValidation: z.enum(['strict', 'warn', 'ignore']).optional().default('warn'),
    visibility: z.enum(['mcp', 'http', 'both']).optional().default('both'),
    license: z.string().optional(),
    compatibility: z.string().max(500).optional(),
    specMetadata: z.record(z.string(), z.string()).optional(),
    allowedTools: z.string().optional(),
    resources: skillResourcesSchema.optional(),
    availableWhen: entryAvailabilitySchema.optional(),
    rating: z.number().min(0).max(5).optional(),
    category: z.string().min(1).max(64).optional(),
    skillPath: z
      .array(
        z
          .string()
          .min(1)
          .max(64)
          // RFC 3986 path segment: unreserved / pct-encoded / sub-delims / ":" / "@".
          // `%` MUST be followed by exactly two hex digits — bare `%` and
          // malformed escapes like `billing%zz` or trailing `%` are rejected.
          .regex(/^(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+$/, {
            message: 'skillPath segment contains characters disallowed by RFC 3986',
          }),
      )
      .min(1)
      .optional(),
  } satisfies RawZodShape<SkillMetadata, ExtendFrontMcpSkillMetadata>)
  .passthrough()
  .superRefine((meta, ctx) => {
    // SEP-2640 §Resource Mapping: final segment of <skill-path> MUST equal
    // the skill's frontmatter `name`. This invariant lets clients recover
    // the name from the URI alone.
    if (meta.skillPath && meta.skillPath.length > 0) {
      const last = meta.skillPath[meta.skillPath.length - 1];
      if (last !== meta.name) {
        ctx.addIssue({
          code: 'custom',
          message: `skillPath final segment "${last}" must equal name "${meta.name}" (SEP-2640 §Resource Mapping)`,
          path: ['skillPath', meta.skillPath.length - 1],
        });
      }
    }
  });

/**
 * Type-safe parsed SkillMetadata from Zod schema.
 */
export type ParsedSkillMetadata = z.output<typeof skillMetadataSchema>;

// ============================================
// Helper Functions
// ============================================

/**
 * Check if a value is a SkillToolRefWithClass (has 'tool' property with a class).
 */
export function isToolRefWithClass(ref: unknown): ref is SkillToolRefWithClass {
  return (
    typeof ref === 'object' &&
    ref !== null &&
    'tool' in ref &&
    typeof (ref as SkillToolRefWithClass).tool === 'function'
  );
}

/**
 * Check if a value is a standard SkillToolRef (has 'name' property).
 */
export function isToolRefWithName(ref: unknown): ref is SkillToolRef {
  return typeof ref === 'object' && ref !== null && 'name' in ref && typeof (ref as SkillToolRef).name === 'string';
}

/**
 * Extract tool name from a tool class decorated with @Tool.
 * Returns undefined if the class is not a valid tool or lacks a name.
 */
export function getToolNameFromClass(toolClass: Type<ToolContext>): string | undefined {
  if (!isClass(toolClass)) return undefined;

  // Try to get the tool name from metadata
  const toolName = getMetadata(FrontMcpToolTokens.name, toolClass);
  if (typeof toolName === 'string') return toolName;

  return undefined;
}

/**
 * Normalize a tool reference to the full SkillToolRef format.
 * Supports: string, tool class, SkillToolRef, or SkillToolRefWithClass.
 *
 * @param ref - The tool reference to normalize
 * @returns Normalized SkillToolRef with name, purpose, and required flag
 * @throws InvalidInputError if tool class doesn't have a valid name
 */
export function normalizeToolRef(ref: SkillToolInput): SkillToolRef {
  // String tool name
  if (typeof ref === 'string') {
    return { name: ref, required: false };
  }

  // Tool class (function)
  if (typeof ref === 'function') {
    const name = getToolNameFromClass(ref as Type<ToolContext>);
    if (!name) {
      throw new InvalidInputError(
        `Invalid tool class '${ref.name ?? 'unknown'}'. ` +
          'Tool class must be decorated with @Tool and have a name property.',
      );
    }
    return { name, required: false };
  }

  // SkillToolRefWithClass: { tool: ToolClass, purpose?, required? }
  if (isToolRefWithClass(ref)) {
    const name = getToolNameFromClass(ref.tool);
    if (!name) {
      throw new InvalidInputError(
        `Invalid tool class in reference. ` + 'Tool class must be decorated with @Tool and have a name property.',
      );
    }
    return {
      name,
      purpose: ref.purpose,
      required: ref.required ?? false,
    };
  }

  // Standard SkillToolRef: { name, purpose?, required? }
  if (isToolRefWithName(ref)) {
    return { ...ref, required: ref.required ?? false };
  }

  throw new InvalidInputError(`Invalid tool reference: ${JSON.stringify(ref)}`);
}

/**
 * Extract tool names from skill metadata.
 * Handles all supported tool reference formats.
 */
export function extractToolNames(metadata: SkillMetadata): string[] {
  if (!metadata.tools) return [];

  return metadata.tools.map((t) => {
    // String name
    if (typeof t === 'string') return t;

    // Tool class
    if (typeof t === 'function') {
      const name = getToolNameFromClass(t as Type<ToolContext>);
      return name ?? `unknown:${t.name}`;
    }

    // SkillToolRefWithClass
    if (isToolRefWithClass(t)) {
      const name = getToolNameFromClass(t.tool);
      return name ?? `unknown:${t.tool.name}`;
    }

    // Standard SkillToolRef
    return (t as SkillToolRef).name;
  });
}

/**
 * Check if instruction source is inline string.
 */
export function isInlineInstructions(source: SkillInstructionSource): source is string {
  return typeof source === 'string';
}

/**
 * Check if instruction source is a file path.
 */
export function isFileInstructions(source: SkillInstructionSource): source is { file: string } {
  return typeof source === 'object' && 'file' in source;
}

/**
 * Check if instruction source is a URL.
 */
export function isUrlInstructions(source: SkillInstructionSource): source is { url: string } {
  return typeof source === 'object' && 'url' in source;
}
