/**
 * Claude client for all generation phases. Streaming + finalMessage (long
 * outputs), adaptive thinking, xhigh effort, structured outputs so every
 * phase returns machine-parseable JSON.
 */
import Anthropic from '@anthropic-ai/sdk';
import type {
  DebugSink,
  DrawOpts,
  FeatureCandidate,
  FeatureNode,
  FeatureProbe,
  FeatureSpec,
  GeneratedFile,
  LlmCallRecord,
  LlmClient,
  RepairInput,
  UsageSink,
} from './types';
import {
  BUILD_SYSTEM,
  CONCEPT_SYSTEM,
  ENCODE_SYSTEM,
  FEATURE_ENUM_SYSTEM,
  FEATURE_PROBE_SYSTEM,
  FEATURE_REPAIR_SYSTEM,
  IR_CASE_FIX_SYSTEM,
  IR_SYSTEM,
  RHA_SYSTEM,
  STUB_FIX_SYSTEM,
  buildUser,
  conceptUser,
  encodeUser,
  featureEnumUser,
  featureProbeUser,
  irCaseFixUser,
  irUser,
  repairFeaturesUser,
  rhaUser,
  stubFixUser,
} from './prompts';
import { CONCEPT_DRAW_TEMP, sanitizeConceptPage } from '../tier1/superbuffer';
import { assembleTestFile } from './irScaffold';
import type { IRCaseGen, IRImport, IRParts } from './irScaffold';
import { STUB_WIRE_SCHEMA, assembleSpecStubs } from './stubScaffold';
import type { WireFeature } from './stubScaffold';
import { getConfig } from './config';

const FEATURE_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['title', 'features'],
  properties: {
    title: { type: 'string' },
    features: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['id', 'name', 'description', 'parentId', 'deps', 'files', 'testFile', 'edgeCases', 'exports', 'envVars', 'stubs'],
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
          description: { type: 'string' },
          parentId: { anyOf: [{ type: 'string' }, { type: 'null' }] },
          deps: { type: 'array', items: { type: 'string' } },
          files: { type: 'array', items: { type: 'string' } },
          testFile: { type: 'string' },
          edgeCases: { type: 'array', items: { type: 'string' } },
          exports: {
            type: 'array',
            items: {
              type: 'object',
              additionalProperties: false,
              required: ['file', 'symbol'],
              properties: { file: { type: 'string' }, symbol: { type: 'string' } },
            },
          },
          envVars: { type: 'array', items: { type: 'string' } },
          // Signatures, not content: the harness assembles the stub file
          // (stubScaffold) so structure can never fail to parse.
          stubs: STUB_WIRE_SCHEMA,
        },
      },
    },
  },
} as const;

/** Bounded repair returns only the offending FeatureNode objects. */
const FEATURE_REPAIR_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['features'],
  properties: {
    features: FEATURE_SCHEMA.properties.features,
  },
} as const;

const IR_PARTS_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['imports', 'cases'],
  properties: {
    imports: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['from', 'symbols'],
        properties: {
          from: { type: 'string' },
          symbols: { type: 'array', items: { type: 'string' } },
        },
      },
    },
    cases: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['name', 'kind', 'body'],
        properties: {
          name: { type: 'string' },
          kind: { type: 'string', enum: ['behavior', 'edge', 'integration'] },
          body: { type: 'string' },
        },
      },
    },
  },
} as const;

const STUB_CONTENT_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['content'],
  properties: { content: { type: 'string' } },
} as const;

const FEATURE_ENUM_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['features'],
  properties: {
    features: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['name', 'scope'],
        properties: { name: { type: 'string' }, scope: { type: 'string' } },
      },
    },
  },
} as const;

const FEATURE_PROBE_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['clear', 'assertion', 'question'],
  properties: {
    clear: { type: 'boolean' },
    assertion: { type: 'string' },
    question: { type: 'string' },
  },
} as const;

const FILES_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['files'],
  properties: {
    files: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['path', 'content'],
        properties: { path: { type: 'string' }, content: { type: 'string' } },
      },
    },
  },
} as const;

/** SET 37f — concept page prose only (never code). */
const CONCEPT_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['concept'],
  properties: {
    concept: { type: 'string' },
  },
} as const;

export class ClaudeClient implements LlmClient {
  private client: Anthropic;
  private modelOverride: string | undefined;

  constructor(
    private onUsage?: UsageSink,
    private onDebug?: DebugSink,
    opts?: { apiKey?: string; model?: string },
  ) {
    this.client = opts?.apiKey ? new Anthropic({ apiKey: opts.apiKey }) : new Anthropic();
    this.modelOverride = opts?.model;
  }

  private async call<T>(
    op: LlmCallRecord['op'],
    featureId: string | null,
    system: string,
    user: string,
    schema: object,
    temperature?: number,
  ): Promise<T> {
    const started = Date.now();
    const { model: cfgModel, effort: EFFORT } = getConfig();
    const MODEL = this.modelOverride || cfgModel;
    const stream = this.client.messages.stream({
      model: MODEL,
      max_tokens: 64000,
      thinking: { type: 'adaptive' },
      output_config: { effort: EFFORT, format: { type: 'json_schema', schema } } as any,
      system,
      messages: [{ role: 'user', content: user }],
      // Decorrelation for swarm/fresh-take draws; Anthropic clamps to [0,1].
      ...(temperature != null ? { temperature: Math.min(1, temperature) } : {}),
    });
    const msg = await stream.finalMessage();
    this.onUsage?.({
      ts: new Date().toISOString(),
      op,
      featureId,
      model: msg.model ?? MODEL,
      ms: Date.now() - started,
      inputTokens: msg.usage?.input_tokens ?? 0,
      outputTokens: msg.usage?.output_tokens ?? 0,
      cacheReadTokens: msg.usage?.cache_read_input_tokens ?? 0,
      cacheWriteTokens: msg.usage?.cache_creation_input_tokens ?? 0,
    });
    if (msg.stop_reason === 'refusal') {
      throw new Error('model declined the request (stop_reason: refusal)');
    }
    const text = msg.content.find(b => b.type === 'text');
    if (!text || text.type !== 'text') throw new Error('no text block in model response');
    this.onDebug?.({
      ts: new Date().toISOString(), op, featureId, model: msg.model ?? MODEL,
      temperature: temperature ?? null, ms: Date.now() - started,
      inputTokens: msg.usage?.input_tokens ?? 0, outputTokens: msg.usage?.output_tokens ?? 0,
      system, user, schemaName: op, responseRaw: text.text,
    });
    return JSON.parse(text.text) as T;
  }

  async enumerateFeatures(prompt: string, opts?: DrawOpts): Promise<FeatureCandidate[]> {
    const out = await this.call<{ features: FeatureCandidate[] }>(
      'encode',
      null,
      FEATURE_ENUM_SYSTEM,
      featureEnumUser(prompt),
      FEATURE_ENUM_SCHEMA,
      opts?.temperature,
    );
    return out.features;
  }

  async probeFeature(candidate: FeatureCandidate, opts?: DrawOpts): Promise<FeatureProbe> {
    return this.call<FeatureProbe>(
      'encode',
      null,
      FEATURE_PROBE_SYSTEM,
      featureProbeUser(candidate.name, candidate.scope),
      FEATURE_PROBE_SCHEMA,
      opts?.temperature,
    );
  }

  async encodeFeatures(prompt: string, stubFeedback?: string, opts?: DrawOpts): Promise<FeatureSpec> {
    const out = await this.call<{ title: string; features: WireFeature[] }>(
      'encode',
      null,
      ENCODE_SYSTEM,
      encodeUser(prompt, stubFeedback),
      FEATURE_SCHEMA,
      opts?.temperature,
    );
    return { title: out.title, prompt, features: assembleSpecStubs(out.features) };
  }

  async repairFeatures(
    spec: FeatureSpec,
    offendingIds: string[],
    problems: string[],
    opts?: DrawOpts,
  ): Promise<FeatureNode[]> {
    const out = await this.call<{ features: WireFeature[] }>(
      'encode',
      null,
      FEATURE_REPAIR_SYSTEM,
      repairFeaturesUser(spec, offendingIds, problems),
      FEATURE_REPAIR_SCHEMA,
      opts?.temperature,
    );
    return assembleSpecStubs(out.features ?? []);
  }

  async fixStub(
    path: string,
    ctx: { diagnostics: string; callingLines?: string; contractText?: string; checkedIdioms?: string },
    opts?: DrawOpts,
  ): Promise<string> {
    const out = await this.call<{ content: string }>(
      'fixStub',
      null,
      STUB_FIX_SYSTEM,
      stubFixUser(path, ctx),
      STUB_CONTENT_SCHEMA,
      opts?.temperature,
    );
    return out.content;
  }

  async generateTests(spec: FeatureSpec, feature: FeatureNode, feedback?: string, opts?: DrawOpts): Promise<{ content: string; parts?: IRParts }> {
    const out = await this.call<{ imports: IRImport[]; cases: IRCaseGen[] }>(
      'ir',
      feature.id,
      IR_SYSTEM,
      irUser(spec, feature, feedback),
      IR_PARTS_SCHEMA,
      opts?.temperature,
    );
    // Deterministic assembly: structure is the harness's job, not the model's.
    return { content: assembleTestFile(feature, out.imports, out.cases), parts: { imports: out.imports, cases: out.cases } };
  }

  async fixTestCases(
    spec: FeatureSpec,
    feature: FeatureNode,
    brokenCases: { name: string; kind: IRCaseGen['kind']; errors: string[] }[],
    importErrors: string[],
    currentImports: IRImport[],
    opts?: DrawOpts,
  ): Promise<{ imports?: IRImport[]; cases: IRCaseGen[] }> {
    const out = await this.call<{ imports?: IRImport[]; cases: IRCaseGen[] }>(
      'ir',
      feature.id,
      IR_CASE_FIX_SYSTEM,
      irCaseFixUser(spec, feature, brokenCases, importErrors, currentImports, opts?.avoid),
      IR_PARTS_SCHEMA,
      opts?.temperature,
    );
    return { imports: out.imports, cases: out.cases };
  }

  async generateImplementation(
    spec: FeatureSpec,
    feature: FeatureNode,
    frozenTest: string,
    dependencyContext: GeneratedFile[],
    opts?: DrawOpts,
  ): Promise<GeneratedFile[]> {
    const out = await this.call<{ files: GeneratedFile[] }>(
      'build',
      feature.id,
      BUILD_SYSTEM,
      buildUser(spec, feature, frozenTest, dependencyContext, opts?.avoid, opts?.context),
      FILES_SCHEMA,
      opts?.temperature,
    );
    return out.files;
  }

  async repair(input: RepairInput, opts?: DrawOpts): Promise<GeneratedFile[]> {
    const out = await this.call<{ files: GeneratedFile[] }>(
      'repair',
      input.feature.id,
      RHA_SYSTEM,
      rhaUser(input),
      FILES_SCHEMA,
      opts?.temperature,
    );
    return out.files;
  }

  /**
   * SET 37f — thin concept draw helper. High-t ideation against the spec page.
   * Returns sanitized prose for the superbuffer concept page (context only).
   */
  async drawConcept(
    specPage: string,
    feature: FeatureNode,
    opts?: DrawOpts,
  ): Promise<string> {
    const out = await this.call<{ concept: string }>(
      'concept',
      feature.id,
      CONCEPT_SYSTEM,
      conceptUser(specPage, feature),
      CONCEPT_SCHEMA,
      opts?.temperature ?? CONCEPT_DRAW_TEMP,
    );
    return sanitizeConceptPage(out.concept ?? '');
  }
}
