/**
 * Base interface for all LLM responses.
 */

import type {
  AssistantContentPart,
  Text,
  Thought,
  ToolCall,
} from "@/llm/content";
import type { DeepPartial, Format } from "@/llm/formatting";
import type { Message } from "@/llm/messages";
import type { Model, Params } from "@/llm/models";
import type { ModelId, ProviderId } from "@/llm/providers";
import type { FinishReason } from "@/llm/responses/finish-reason";
import type { Usage } from "@/llm/responses/usage";

import { ParseError } from "@/llm/exceptions";
import { extractSerializedJson, parsePartial } from "@/llm/responses/utils";

/**
 * Type alias for RootResponse with any format type.
 * Useful for functions that accept any response type.
 */
export type AnyResponse = RootResponse<unknown>;

/**
 * Base class for LLM responses.
 *
 * This abstract class defines the core interface that all response types must implement.
 * It provides the common properties and methods shared across all response variants.
 *
 * @template F - The type of the formatted output when using structured outputs.
 */
export abstract class RootResponse<F = unknown> {
  /**
   * The raw response from the LLM.
   */
  abstract readonly raw: unknown;

  /**
   * The provider that generated this response.
   */
  abstract readonly providerId: ProviderId;

  /**
   * The model ID that generated this response.
   */
  abstract readonly modelId: ModelId;

  /**
   * Provider-specific model name (may include additional info like API mode).
   */
  abstract readonly providerModelName: string;

  /**
   * The parameters used to generate this response.
   */
  abstract readonly params: Params;

  /**
   * The message history, including the most recent assistant message.
   */
  abstract readonly messages: readonly Message[];

  /**
   * The content generated by the LLM.
   */
  abstract readonly content: readonly AssistantContentPart[];

  /**
   * The text content in the generated response, if any.
   */
  abstract readonly texts: readonly Text[];

  /**
   * The tools the LLM wants called on its behalf, if any.
   */
  abstract readonly toolCalls: readonly ToolCall[];

  /**
   * The readable thoughts from the model's thinking process, if any.
   *
   * The thoughts may be direct output from the model thinking process, or may be a
   * generated summary. (This depends on the provider; newer models tend to summarize.)
   */
  abstract readonly thoughts: readonly Thought[];

  /**
   * The reason why the LLM finished generating a response, if set.
   *
   * `finishReason` is only set if the response did not finish generating normally,
   * e.g. `FinishReason.MAX_TOKENS` if the model ran out of tokens before completing.
   * When the response generates normally, `response.finishReason` will be `null`.
   */
  abstract readonly finishReason: FinishReason | null;

  /**
   * Token usage statistics for this response, if available.
   */
  abstract readonly usage: Usage | null;

  /**
   * The Format describing the structured response format, if available.
   *
   * When a format is specified in the call, this contains the resolved Format
   * with schema and validator information used for parsing.
   */
  abstract readonly format: Format | null;

  /**
   * Return all text content from this response as a single string.
   *
   * Joins the text from all `Text` parts in the response content using the
   * specified separator.
   *
   * @param sep - The separator to use when joining multiple text parts.
   *              Defaults to newline ("\n").
   * @returns A string containing all text content joined by the separator.
   *          Returns an empty string if the response contains no text parts.
   *
   * @example
   * ```typescript
   * response.text()        // Join with newlines (default): "Hello\nWorld"
   * response.text(" ")     // Join with spaces: "Hello World"
   * response.text("")      // Concatenate directly: "HelloWorld"
   * ```
   */
  text(sep: string = "\n"): string {
    return this.texts.map((t) => t.text).join(sep);
  }

  /**
   * Return a string representation of all response content.
   *
   * The response content will be represented in a way that emphasizes clarity and
   * readability, but may not include all metadata (like thinking signatures or tool
   * call ids), and thus cannot be used to reconstruct the response.
   *
   * @example
   * ```
   * **Thinking:**
   *   The user is asking a math problem. I should use the calculator tool.
   *
   * **ToolCall (calculator):** {"operation": "mult", "a": 1337, "b": 4242}
   *
   * I am going to use the calculator and answer your question for you!
   * ```
   */
  pretty(): string {
    const parts: string[] = [];

    for (const part of this.content) {
      if (part.type === "text") {
        parts.push(part.text);
      } else if (part.type === "tool_call") {
        parts.push(`**ToolCall (${part.name}):** ${part.args}`);
      } else if (part.type === "thought") {
        const indented = part.thought
          .split("\n")
          .map((line) => `  ${line}`)
          .join("\n");
        parts.push(`**Thinking:**\n${indented}`);
      }
    }

    return parts.join("\n\n");
  }

  /**
   * A Model with parameters matching this response.
   *
   * Creates a new Model instance with the same model ID and parameters that
   * were used to generate this response. This is useful for resuming
   * conversations or making follow-up calls with consistent settings.
   *
   * Uses dynamic import to avoid circular dependencies.
   *
   * @returns A Promise resolving to a Model instance configured with this response's model ID and params.
   *
   * @example
   * ```typescript
   * const response = await model.call('Hello!');
   * const sameModel = await response.model;
   * const followUp = await sameModel.call('Tell me more');
   * ```
   */
  get model(): Promise<Model> {
    return (async () => {
      const { Model: ModelClass } = await import("@/llm/models/model");
      return new ModelClass(this.modelId, this.params);
    })();
  }

  /**
   * Parse the response with partial parsing enabled (for streaming).
   *
   * Use this during streaming to parse incomplete JSON as it arrives.
   * Returns a DeepPartial version where all fields are optional.
   *
   * @returns The partially parsed response with optional fields.
   *
   * @example
   * ```typescript
   * for await (const chunk of streamResponse.chunkStream()) {
   *   const partial = streamResponse.parse({ partial: true });
   *   // partial.title may be undefined initially
   * }
   * ```
   */
  parse(options: { partial: true }): DeepPartial<F>;

  /**
   * Parse the response according to the response format.
   *
   * If a format was specified in the call, parses the response content
   * according to that format. For JSON-based formats, extracts and parses
   * the JSON. For OutputParser formats, calls the custom parser.
   *
   * When a format is specified, returns the parsed value of type F.
   * When no format is specified (F = unknown), returns null at runtime.
   *
   * @returns The parsed response.
   * @throws ParseError if parsing fails.
   *
   * @example
   * ```typescript
   * interface Book { title: string; author: string; }
   *
   * const response = await model.call<Book>('Recommend a book', {
   *   format: BookSchema
   * });
   *
   * const book = response.parse(); // Type: Book
   * console.log(book.title);
   * ```
   */
  parse(options?: { partial?: false }): F;

  // Implementation
  parse(options?: { partial?: boolean }): F | DeepPartial<F> | null {
    // No format specified - return null
    if (!this.format) {
      return null;
    }

    // Handle OutputParser
    if (this.format.outputParser) {
      if (options?.partial) {
        throw new Error(
          "parse({ partial: true }) is not supported with OutputParser",
        );
      }
      try {
        return this.format.outputParser(this) as F;
      } catch (e) {
        const error = e instanceof Error ? e : new Error(String(e));
        throw new ParseError(`OutputParser failed: ${error.message}`, error);
      }
    }

    // Get text content
    const textContent = this.text("");

    // Partial parsing for streaming
    if (options?.partial) {
      const partialResult = parsePartial<DeepPartial<F>>(
        textContent,
        this.format.validator ?? undefined,
        this.format.toolSchemaUnwrapKey ?? undefined,
      );
      return partialResult;
    }

    // Full parsing
    try {
      const jsonText = extractSerializedJson(textContent);
      let parsed = JSON.parse(jsonText) as unknown;

      // Unwrap if schema was wrapped (non-object types like arrays)
      if (
        this.format.toolSchemaUnwrapKey &&
        typeof parsed === "object" &&
        parsed !== null
      ) {
        parsed = (parsed as Record<string, unknown>)[
          this.format.toolSchemaUnwrapKey
        ];
      }

      // Validate with Zod if validator provided
      if (this.format.validator) {
        const result = this.format.validator.safeParse(parsed);
        if (!result.success) {
          throw new Error(`Validation failed: ${JSON.stringify(result.error)}`);
        }
        return result.data as F;
      }

      return parsed as F;
    } catch (e) {
      /* v8 ignore next -- defensive: JSON.parse always throws Error */
      const error = e instanceof Error ? e : new Error(String(e));
      throw new ParseError(`Failed to parse response: ${error.message}`, error);
    }
  }
}
