/**
 * Skill generation service using Nyx Provider (Responses API format).
 * Replaces Vercel AI SDK and OpenAI SDK with NyxLlmClient.
 * Streams generation events via SSE. Does NOT auto-persist.
 * @module domains/skills/generation/service
 */

import { z } from "zod";
import type { NyxLlmClient, ResponsesApiStreamEvent, ResponsesApiInputMessage } from "../../../clients/nyxid/llm";
import type { GeneratedSkill, SkillStreamEvent } from "../../../shared/types/index";
import {
  buildDirectGenerationPrompt,
  buildOpenApiGenerationPrompt,
  buildSourceCodeGenerationPrompt,
  GENERATION_SYSTEM_PROMPT,
  OPENAPI_GENERATION_SYSTEM_PROMPT,
  SOURCE_CODE_GENERATION_SYSTEM_PROMPT,
} from "./prompts";
import pino from "pino";

const logger = pino({ level: "info" }).child({ module: "skillGenerationService" });

const generatedSkillSchema = z.object({
  name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
  description: z.string().min(10).max(500),
  category: z.enum(["plain", "runtime-based"]),
  outputType: z.enum(["text", "file"]).optional(),
  tags: z.array(z.string().min(2).max(30).regex(/^[a-z0-9-]+$/)).min(1).max(10),
  readmeBody: z.string().min(50).max(20_000),
  runtimes: z.array(z.string()).default([]),
  dependencies: z.array(z.string().max(200)).default([]),
  envVars: z.array(z.string().max(100)).default([]),
  scripts: z.array(z.object({
    filename: z.string().min(1).max(200),
    content: z.string().min(1).max(50_000),
  })).default([]),
});

export { generatedSkillSchema };

/**
 * Per-call resolution of LLM defaults from admin settings (`skillGen`
 * section + selected provider's `maxOutputTokens` / `defaultTemperature`).
 * Pulled fresh on every generation so an admin can swap the default
 * provider without a redeploy.
 */
export interface SkillGenLlmDefaults {
  model: string;
  maxOutputTokens: number;
  temperature: number;
}

export type SkillGenLlmDefaultsResolver = () => Promise<SkillGenLlmDefaults>;

export interface GenerationServiceConfig {
  llmClient: NyxLlmClient;
  /**
   * Resolver returning the active LLM defaults for the skill-generation
   * surface. Read on every generate* call. NO env fallback: the route
   * surfaces an explicit error to the caller when settings are missing.
   */
  defaultsResolver: SkillGenLlmDefaultsResolver;
}

export class SkillGenerationService {
  private readonly llmClient: NyxLlmClient;
  private readonly defaultsResolver: SkillGenLlmDefaultsResolver;

  constructor(config: GenerationServiceConfig) {
    this.llmClient = config.llmClient;
    this.defaultsResolver = config.defaultsResolver;
  }

  private async resolveDefaults(): Promise<SkillGenLlmDefaults> {
    const d = await this.defaultsResolver();
    if (!d.model || d.model.trim().length === 0) {
      throw new Error(
        "SKILLGEN_LLM_NOT_CONFIGURED: default model not set in /admin/settings/skill-generation",
      );
    }
    return d;
  }

  /**
   * Direct generation streaming. Streams tokens via SSE events.
   * Uses Nyx Provider Responses API format. `modelOverride` (when set)
   * picks an admin-curated model; otherwise the service-level default
   * applies.
   */
  async *generateStream(
    query: string,
    signal?: AbortSignal,
    modelOverride?: string,
  ): AsyncIterable<SkillStreamEvent> {
    let defaults: SkillGenLlmDefaults;
    try {
      defaults = await this.resolveDefaults();
    } catch (err) {
      yield { type: "error", message: (err as Error).message };
      return;
    }
    const model = modelOverride ?? defaults.model;
    if (signal?.aborted) {
      yield { type: "error", message: "Request aborted" };
      return;
    }

    yield { type: "generation_start" };

    const { userPrompt } = buildDirectGenerationPrompt(query);
    const input: ResponsesApiInputMessage[] = [
      { role: "developer", content: GENERATION_SYSTEM_PROMPT },
      { role: "user", content: userPrompt },
    ];

    let accumulated = "";

    try {
      const streamEvents = this.llmClient.stream({
        model,
        input,
        max_output_tokens: defaults.maxOutputTokens,
        temperature: defaults.temperature,
      });

      for await (const event of streamEvents) {
        if (signal?.aborted) {
          yield { type: "error", message: "Request aborted" };
          return;
        }

        const text = extractTextFromEvent(event);
        if (text) {
          accumulated += text;
          yield { type: "token", content: text };
        }
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      logger.error({ err: message }, "LLM stream error");
      yield { type: "error", message: `LLM error: ${message}` };
      return;
    }

    // Validate the accumulated output
    const parsed = this.parseAndValidate(accumulated);
    if (!parsed) {
      logger.warn("LLM output failed validation, attempting retry");
      yield { type: "validation_error", message: "Invalid JSON from LLM", retrying: true };

      // Retry with non-streaming complete call
      if (!signal?.aborted) {
        try {
          const retryInput: ResponsesApiInputMessage[] = [
            { role: "developer", content: GENERATION_SYSTEM_PROMPT },
            { role: "user", content: `${userPrompt}\n\nIMPORTANT: Output ONLY valid JSON. No markdown fences. No extra text.` },
          ];

          const outputs = await this.llmClient.complete({
            model,
            input: retryInput,
            max_output_tokens: defaults.maxOutputTokens,
            temperature: defaults.temperature,
          });

          let retryText = "";
          for (const output of outputs) {
            if (output.content) {
              for (const part of output.content) {
                if (part.text) retryText += part.text;
              }
            }
          }

          const retryParsed = this.parseAndValidate(retryText);
          if (retryParsed) {
            yield { type: "generation_complete", raw: retryText };
            return;
          }
        } catch (retryErr) {
          const msg = retryErr instanceof Error ? retryErr.message : String(retryErr);
          logger.error({ err: msg }, "LLM retry error");
          yield { type: "error", message: `LLM retry error: ${msg}` };
          return;
        }
      }

      yield { type: "error", message: "LLM produced invalid output after retry" };
      return;
    }

    yield { type: "generation_complete", raw: accumulated };
  }

  /**
   * Multi-turn generation. Converts message history to Responses API format.
   */
  async *generateStreamWithHistory(
    messages: Array<{ role: "user" | "assistant"; content: string }>,
    signal?: AbortSignal,
    modelOverride?: string,
  ): AsyncIterable<SkillStreamEvent> {
    let defaults: SkillGenLlmDefaults;
    try {
      defaults = await this.resolveDefaults();
    } catch (err) {
      yield { type: "error", message: (err as Error).message };
      return;
    }
    const model = modelOverride ?? defaults.model;
    if (signal?.aborted) {
      yield { type: "error", message: "Request aborted" };
      return;
    }

    yield { type: "generation_start" };

    // Put system prompt as developer message in input array (not as instructions)
    // because some LLM providers ignore the instructions field.
    const input: ResponsesApiInputMessage[] = [
      { role: "developer", content: GENERATION_SYSTEM_PROMPT },
      ...messages.map((m, i) => {
        if (i === 0 && m.role === "user") {
          return {
            role: "user" as const,
            content: `Generate a skill for: "${m.content}"`,
          };
        }
        return {
          role: m.role === "assistant" ? "assistant" as const : "user" as const,
          content: m.content,
        };
      }),
    ];

    let accumulated = "";

    try {
      const streamEvents = this.llmClient.stream({
        model,
        input,
        max_output_tokens: defaults.maxOutputTokens,
        temperature: defaults.temperature,
      });

      for await (const event of streamEvents) {
        if (signal?.aborted) {
          yield { type: "error", message: "Request aborted" };
          return;
        }

        const text = extractTextFromEvent(event);
        if (text) {
          accumulated += text;
          yield { type: "token", content: text };
        }
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      logger.error({ err: message }, "LLM multi-turn stream error");
      yield { type: "error", message: `LLM error: ${message}` };
      return;
    }

    logger.info(
      { accumulatedLength: accumulated.length, first200: accumulated.slice(0, 200), last200: accumulated.slice(-200) },
      "Multi-turn generation accumulated text",
    );

    const parsed = this.parseAndValidate(accumulated);
    if (!parsed) {
      logger.warn({ first500: accumulated.slice(0, 500) }, "Multi-turn validation failed");
      yield { type: "validation_error", message: "Invalid JSON from LLM", retrying: false };
    } else {
      logger.info({ skillName: parsed.name }, "Multi-turn validation passed");
    }

    yield { type: "generation_complete", raw: accumulated };
  }

  /**
   * Generate a skill from an OpenAPI spec. Streams tokens via SSE events.
   */
  async *generateFromOpenApi(
    specContent: string,
    options?: { endpoints?: string[]; description?: string },
    signal?: AbortSignal,
    modelOverride?: string,
  ): AsyncIterable<SkillStreamEvent> {
    let defaults: SkillGenLlmDefaults;
    try {
      defaults = await this.resolveDefaults();
    } catch (err) {
      yield { type: "error", message: (err as Error).message };
      return;
    }
    const model = modelOverride ?? defaults.model;
    if (signal?.aborted) {
      yield { type: "error", message: "Request aborted" };
      return;
    }

    yield { type: "generation_start" };

    const userPrompt = buildOpenApiGenerationPrompt(specContent, options);
    const input: ResponsesApiInputMessage[] = [
      { role: "developer", content: OPENAPI_GENERATION_SYSTEM_PROMPT },
      { role: "user", content: userPrompt },
    ];

    let accumulated = "";

    try {
      const streamEvents = this.llmClient.stream({
        model,
        input,
        max_output_tokens: defaults.maxOutputTokens,
        temperature: defaults.temperature,
      });

      for await (const event of streamEvents) {
        if (signal?.aborted) {
          yield { type: "error", message: "Request aborted" };
          return;
        }

        const text = extractTextFromEvent(event);
        if (text) {
          accumulated += text;
          yield { type: "token", content: text };
        }
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      logger.error({ err: message }, "OpenAPI generation LLM stream error");
      yield { type: "error", message: `LLM error: ${message}` };
      return;
    }

    const parsed = this.parseAndValidate(accumulated);
    if (!parsed) {
      logger.warn("OpenAPI generation output failed validation");
      yield { type: "validation_error", message: "Invalid JSON from LLM", retrying: false };
    }

    yield { type: "generation_complete", raw: accumulated };
  }

  /**
   * Generate a skill from raw backend source code (route / controller /
   * handler files). Streams tokens via the same SSE event vocabulary as
   * the other generators.
   *
   * `code` is typically a concatenation of several source files, each
   * preceded by a `// FILE: <path>` marker — that's exactly what
   * {@link fetchGithubSourceBundle} produces.
   */
  async *generateFromSource(
    code: string,
    options?: { framework?: string; description?: string; sourceUrl?: string },
    signal?: AbortSignal,
    modelOverride?: string,
  ): AsyncIterable<SkillStreamEvent> {
    let defaults: SkillGenLlmDefaults;
    try {
      defaults = await this.resolveDefaults();
    } catch (err) {
      yield { type: "error", message: (err as Error).message };
      return;
    }
    const model = modelOverride ?? defaults.model;
    if (signal?.aborted) {
      yield { type: "error", message: "Request aborted" };
      return;
    }

    yield { type: "generation_start" };

    const userPrompt = buildSourceCodeGenerationPrompt(code, options);
    const input: ResponsesApiInputMessage[] = [
      { role: "developer", content: SOURCE_CODE_GENERATION_SYSTEM_PROMPT },
      { role: "user", content: userPrompt },
    ];

    let accumulated = "";

    try {
      const streamEvents = this.llmClient.stream({
        model,
        input,
        max_output_tokens: defaults.maxOutputTokens,
        temperature: defaults.temperature,
      });

      for await (const event of streamEvents) {
        if (signal?.aborted) {
          yield { type: "error", message: "Request aborted" };
          return;
        }

        const text = extractTextFromEvent(event);
        if (text) {
          accumulated += text;
          yield { type: "token", content: text };
        }
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      logger.error({ err: message }, "Source-code generation LLM stream error");
      yield { type: "error", message: `LLM error: ${message}` };
      return;
    }

    const parsed = this.parseAndValidate(accumulated);
    if (!parsed) {
      logger.warn("Source-code generation output failed validation");
      yield { type: "validation_error", message: "Invalid JSON from LLM", retrying: false };
    }

    yield { type: "generation_complete", raw: accumulated };
  }

  parseAndValidate(raw: string): GeneratedSkill | null {
    try {
      let cleaned = raw.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();

      const jsonStart = cleaned.indexOf("{");
      const jsonEnd = cleaned.lastIndexOf("}");
      if (jsonStart >= 0 && jsonEnd > jsonStart) {
        cleaned = cleaned.slice(jsonStart, jsonEnd + 1);
      }

      const json = JSON.parse(cleaned);

      // Handle backward-compat: rename readmeMd -> readmeBody
      if (json.readmeMd && !json.readmeBody) {
        const md = json.readmeMd as string;
        const fmEnd = md.indexOf("\n---", 3);
        json.readmeBody = fmEnd > 0 ? md.slice(fmEnd + 4).trim() : md;
        delete json.readmeMd;
      }

      const result = generatedSkillSchema.safeParse(json);
      if (!result.success) {
        logger.debug({ errors: result.error.issues }, "Generated skill validation failed");
        return null;
      }

      return result.data;
    } catch {
      return null;
    }
  }
}

/**
 * Extract text content from a Responses API stream event.
 * Handles response.output_text.delta and response.content_part.delta events.
 */
function extractTextFromEvent(event: ResponsesApiStreamEvent): string | null {
  const eventType = event.type;

  if (eventType === "response.output_text.delta") {
    return (event as any).delta ?? null;
  }

  if (eventType === "response.content_part.delta") {
    const delta = (event as any).delta;
    if (delta && typeof delta === "object" && delta.type === "output_text" && typeof delta.text === "string") {
      return delta.text;
    }
  }

  return null;
}
