import {
  findProviderDefinition,
  formatSupportedProviderTypes,
  getProviderCredentialRequirement,
} from '@robota-sdk/agent-core';
import { formatEnvReference, hasUsableSecretReference } from '@robota-sdk/agent-core';

import type {
  IProviderDefinition,
  IProviderCredentialRequirement,
  IProviderProfileConfig,
  TProviderCredentialField,
  TUniversalValue,
} from '@robota-sdk/agent-core';

export interface IProviderProfileSettings extends IProviderProfileConfig {
  [key: string]: TUniversalValue;
}

export interface ILegacyProviderSettings {
  [key: string]: TUniversalValue;
  name?: string;
  model?: string;
  apiKey?: string;
  baseURL?: string;
  timeout?: number;
  options?: Record<string, TUniversalValue>;
}

export type TProviderSettingsDocument = Record<string, TUniversalValue> & {
  currentProvider?: string;
  providers?: Record<string, IProviderProfileSettings>;
  provider?: ILegacyProviderSettings;
};

export interface IProviderSetupInput {
  profile: string;
  type: string;
  model?: string;
  apiKey?: string;
  apiKeyEnv?: string;
  baseURL?: string;
  timeout?: number;
  setCurrent?: boolean;
}

export interface IProviderSetupPatch {
  currentProvider?: string;
  providers: Record<string, IProviderProfileSettings>;
}

export interface IProviderSettingsBuildOptions {
  providerDefinitions?: readonly IProviderDefinition[];
  /** Environment map for configure-time checks (test seam, default: process.env). */
  env?: Record<string, string | undefined>;
}

export function upsertProviderProfile(
  settings: TProviderSettingsDocument,
  profileName: string,
  profile: IProviderProfileSettings,
): TProviderSettingsDocument {
  return {
    ...settings,
    providers: {
      ...(settings.providers ?? {}),
      [profileName]: profile,
    },
  };
}

export function setCurrentProvider(
  settings: TProviderSettingsDocument,
  profileName: string,
): TProviderSettingsDocument {
  if (!settings.providers?.[profileName]) {
    throw new Error(`Provider profile "${profileName}" was not found`);
  }
  return {
    ...settings,
    currentProvider: profileName,
  };
}

export function deleteProviderProfile(
  settings: TProviderSettingsDocument,
  profileName: string,
  options: { replacementCurrentProvider?: string } = {},
): TProviderSettingsDocument {
  if (!settings.providers?.[profileName]) {
    throw new Error(`Provider profile "${profileName}" was not found`);
  }
  const providers = { ...settings.providers };
  delete providers[profileName];
  if (
    options.replacementCurrentProvider !== undefined &&
    providers[options.replacementCurrentProvider] === undefined
  ) {
    throw new Error(`Provider profile "${options.replacementCurrentProvider}" was not found`);
  }
  const next: TProviderSettingsDocument = {
    ...settings,
    providers,
  };
  if (settings.currentProvider !== profileName) {
    return next;
  }
  if (options.replacementCurrentProvider !== undefined) {
    return {
      ...next,
      currentProvider: options.replacementCurrentProvider,
    };
  }
  const withoutCurrentProvider = { ...next };
  delete withoutCurrentProvider.currentProvider;
  return withoutCurrentProvider;
}

export function validateProviderProfile(
  profileName: string,
  profile: IProviderProfileSettings,
  options: IProviderSettingsBuildOptions = {},
): void {
  if (!profile.type) {
    throw new Error(`Provider profile "${profileName}" is missing type`);
  }
  // CLI-068: causal order — an unknown provider must be diagnosed as unknown,
  // not as a missing field that its (nonexistent) defaults failed to fill.
  const definitions = options.providerDefinitions ?? [];
  const definition = findProviderDefinition(definitions, profile.type);
  if (definition === undefined && definitions.length > 0) {
    throw new Error(
      `Unknown provider "${profile.type}". Supported providers: ${formatSupportedProviderTypes(definitions)}`,
    );
  }
  if (!profile.model) {
    throw new Error(`Provider profile "${profileName}" is missing model`);
  }
  const credentialRequirement = getProviderCredentialRequirement(definition);
  if (
    credentialRequirement !== undefined &&
    !hasUsableRequiredProviderCredential(profile, definition?.defaults, credentialRequirement)
  ) {
    throw new Error(
      `Provider profile "${profileName}" is missing ${formatCredentialRequirement(credentialRequirement)}`,
    );
  }
}

export function buildProviderSetupPatch(
  input: IProviderSetupInput,
  options: IProviderSettingsBuildOptions = {},
): IProviderSetupPatch {
  const profile = buildProviderProfile(input, options);
  validateProviderProfile(input.profile, profile, options);
  return {
    ...(input.setCurrent && { currentProvider: input.profile }),
    providers: {
      [input.profile]: profile,
    },
  };
}

export function buildProviderProfile(
  input: IProviderSetupInput,
  options: IProviderSettingsBuildOptions = {},
): IProviderProfileSettings {
  const defaults = getProviderDefaults(input.type, options.providerDefinitions ?? []);
  if (input.apiKey !== undefined && input.apiKeyEnv === undefined) {
    // eslint-disable-next-line no-console -- security warning surfaced to CLI user at setup time; no logger injected in this pure-function context
    console.warn(
      'API key stored as plain text in settings. Use --api-key-env for better security.',
    ); // allow-console: security warning at setup time
  }
  if (input.apiKeyEnv !== undefined) {
    // CLI-068: the referenced variable must exist when configuring — failing at
    // first run with a generic missing-apiKey message diagnoses the wrong cause.
    const envValue = (options.env ?? process.env)[input.apiKeyEnv];
    if (envValue === undefined || envValue.length === 0) {
      throw new Error(
        `Environment variable ${input.apiKeyEnv} is not set — set it before configuring ` +
          `(the profile will reference $ENV:${input.apiKeyEnv})`,
      );
    }
  }
  const apiKey =
    input.apiKeyEnv !== undefined
      ? formatEnvReference(input.apiKeyEnv)
      : (input.apiKey ?? defaults.apiKey);
  const baseURL = input.baseURL ?? defaults.baseURL;

  return {
    type: input.type,
    model: input.model ?? defaults.model,
    ...(isNonEmptyString(apiKey) && { apiKey }),
    ...(baseURL !== undefined && { baseURL }),
    ...(input.timeout !== undefined && { timeout: input.timeout }),
  };
}

export function mergeProviderPatch(
  settings: TProviderSettingsDocument,
  patch: IProviderSetupPatch,
): TProviderSettingsDocument {
  const [profileName, profile] = Object.entries(patch.providers)[0] ?? [];
  if (!profileName || !profile) {
    return settings;
  }
  const withProfile = upsertProviderProfile(settings, profileName, profile);
  return patch.currentProvider
    ? setCurrentProvider(withProfile, patch.currentProvider)
    : withProfile;
}

function getProviderDefaults(
  type: string,
  providerDefinitions: readonly IProviderDefinition[],
): { model?: string; apiKey?: string; baseURL?: string } {
  return findProviderDefinition(providerDefinitions, type)?.defaults ?? {};
}

function hasUsableRequiredProviderCredential(
  profile: IProviderProfileSettings,
  defaults: IProviderDefinition['defaults'],
  requirement: IProviderCredentialRequirement,
): boolean {
  return requirement.anyOf.some((field) =>
    hasUsableSecretReference(resolveProviderCredentialValue(field, profile, defaults)),
  );
}

function resolveProviderCredentialValue(
  field: TProviderCredentialField,
  profile: IProviderProfileSettings,
  defaults: IProviderDefinition['defaults'],
): string | undefined {
  return profile[field] ?? defaults?.[field];
}

function formatCredentialRequirement(requirement: IProviderCredentialRequirement): string {
  return requirement.anyOf.join(' or ');
}

function isNonEmptyString(value: string | undefined): value is string {
  return value !== undefined && value.length > 0;
}
