import { cleanInstructions } from '../../types/propagation';
import { cleanPlantDisplayName } from '../../utils/plantHelpers';
import { createTimestampId, warnDev } from '../providerUtils';
import {
  getPlantScanResultImageUris,
  getPreferredApproximateLocationText,
  getPrimaryPlantScanImageUri,
  normalizeOptionalText,
} from './inputNormalization';
import {
  PlantIdentificationError,
  type PlantIdentificationInput,
  type PlantIdentificationResult,
  type PlantIdentificationSuggestion,
  type SeedIdentificationResult,
} from './types';

export type XiaomiChatResponse = {
  id?: unknown;
  choices?: unknown;
};

type ParsedIdentification = {
  isPlant?: boolean;
  commonName?: string;
  scientificName?: string;
  confidence: number;
  evidence?: string;
  uncertainty?: string;
  alternates: string[];
  propagation?: PlantIdentificationResult['propagation'];
  petSafety?: string;
  kingdom?: PlantIdentificationResult['kingdom'];
  edibility?: PlantIdentificationResult['edibility'];
  cultivable?: boolean;
  substrate?: string;
  raw: Record<string, unknown>;
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}

function readString(value: unknown, maxLength = 8_000): string | undefined {
  if (typeof value !== 'string') return undefined;
  const normalized = value.trim();
  return normalized ? normalized.slice(0, maxLength) : undefined;
}

function isUnknownLabel(value: string): boolean {
  return /^(unknown|unsure|n\/a|not sure)$/i.test(value.trim());
}

function readKnownName(value: unknown, cleanDisplayName = false): string | undefined {
  const name = readString(value, 300);
  if (!name || isUnknownLabel(name)) return undefined;

  const cleaned = cleanDisplayName ? cleanPlantDisplayName(name) : name;
  return cleaned?.trim() || undefined;
}

function toConfidencePercent(value: unknown): number {
  if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
  const percentage = value > 0 && value <= 1 ? value * 100 : value;
  return Math.max(0, Math.min(100, Math.round(percentage)));
}

function readOptionalConfidence(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value)
    ? toConfidencePercent(value)
    : undefined;
}

function readStringArray(value: unknown, maximum = Number.POSITIVE_INFINITY): string[] {
  if (!Array.isArray(value)) return [];

  const seen = new Set<string>();
  const result: string[] = [];

  for (const item of value) {
    const text = readString(item, 2_000);
    if (!text || isUnknownLabel(text)) continue;

    const key = text.toLocaleLowerCase('en-US');
    if (seen.has(key)) continue;
    seen.add(key);
    result.push(text);
    if (result.length >= maximum) break;
  }

  return result;
}

function readLowercaseEnum<T extends string>(
  value: unknown,
  allowedValues: readonly T[],
): T | undefined {
  const normalized = readString(value, 100)?.toLowerCase();
  return normalized && (allowedValues as readonly string[]).includes(normalized)
    ? normalized as T
    : undefined;
}

function extractBalancedObject(text: string, startIndex: number): string | null {
  let depth = 0;
  let inString = false;
  let escaped = false;

  for (let index = startIndex; index < text.length; index += 1) {
    const character = text[index];

    if (inString) {
      if (escaped) {
        escaped = false;
      } else if (character === '\\') {
        escaped = true;
      } else if (character === '"') {
        inString = false;
      }
      continue;
    }

    if (character === '"') {
      inString = true;
    } else if (character === '{') {
      depth += 1;
    } else if (character === '}') {
      depth -= 1;
      if (depth === 0) return text.slice(startIndex, index + 1);
      if (depth < 0) return null;
    }
  }

  return null;
}

function parseJsonObjectCandidate(text: string): Record<string, unknown> | null {
  for (let index = text.indexOf('{'); index >= 0; index = text.indexOf('{', index + 1)) {
    const objectText = extractBalancedObject(text, index);
    if (!objectText) continue;

    try {
      const parsed: unknown = JSON.parse(objectText);
      if (isRecord(parsed)) return parsed;
    } catch {
      // Continue scanning: prose or a fenced reasoning block may contain braces
      // before the actual JSON response.
    }
  }

  return null;
}

function parseJsonObject(content: string): Record<string, unknown> {
  const trimmed = content.trim();
  const fencedBlocks = Array.from(trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi));

  for (const match of fencedBlocks) {
    const parsed = parseJsonObjectCandidate(match[1] || '');
    if (parsed) return parsed;
  }

  const parsed = parseJsonObjectCandidate(trimmed);
  if (parsed) return parsed;
  throw new Error('No valid JSON object was found in the model response.');
}

function parsePropagationMethod(
  value: unknown,
): NonNullable<PlantIdentificationResult['propagation']>['methods'][number] | null {
  if (!isRecord(value)) return null;

  const method = readString(value.method, 300);
  if (!method) return null;

  const rawInstructions = readStringArray(value.instructions, 30);
  const cleanedInstructions = cleanInstructions(rawInstructions);
  const warning = value.warning === null ? null : readString(value.warning, 2_000);
  const risks = readString(value.risks, 2_000);

  return {
    method,
    difficulty: readString(value.difficulty, 200) || '',
    ...(cleanedInstructions?.length ? { instructions: cleanedInstructions } : {}),
    timeframe: readString(value.timeframe, 500) || '',
    successRate: readString(value.successRate, 500) || '',
    ...(risks ? { risks } : {}),
    ...(warning !== undefined ? { warning } : {}),
  };
}

function parseNotRecommendedMethod(
  value: unknown,
): NonNullable<PlantIdentificationResult['propagation']>['notRecommended'][number] | null {
  if (!isRecord(value)) return null;

  const method = readString(value.method, 300);
  const reason = readString(value.reason, 2_000);
  return method && reason ? { method, reason } : null;
}

function parsePropagation(value: unknown): PlantIdentificationResult['propagation'] | undefined {
  if (!isRecord(value)) return undefined;

  const methods = Array.isArray(value.methods)
    ? value.methods
      .map(parsePropagationMethod)
      .filter((method): method is NonNullable<typeof method> => method !== null)
    : [];
  const notRecommended = Array.isArray(value.notRecommended)
    ? value.notRecommended
      .map(parseNotRecommendedMethod)
      .filter((method): method is NonNullable<typeof method> => method !== null)
    : [];
  const canPropagate = typeof value.canPropagate === 'boolean'
    ? value.canPropagate
    : methods.length > 0;
  const generalNote = readString(value.generalNote, 4_000);

  if (
    typeof value.canPropagate !== 'boolean'
    && methods.length === 0
    && notRecommended.length === 0
    && !generalNote
  ) {
    return undefined;
  }

  return {
    canPropagate,
    methods,
    notRecommended,
    ...(generalNote ? { generalNote } : {}),
  };
}

function parseSeedTreatment(
  value: unknown,
  includeDuration: boolean,
): { needed: boolean; method: string; duration?: string } | undefined {
  if (!isRecord(value)) return undefined;

  const hasNeededFlag = typeof value.needed === 'boolean';
  const needed = hasNeededFlag ? value.needed as boolean : false;
  const method = readString(value.method, 1_000) || '';
  const duration = includeDuration ? readString(value.duration, 500) || '' : undefined;

  if (!hasNeededFlag && !method && !duration) return undefined;

  if (includeDuration) {
    return { needed, method, duration: duration || '' };
  }

  return { needed, method };
}

function parseSeedPropagationMethods(
  value: unknown,
): NonNullable<SeedIdentificationResult['propagationMethods']> | undefined {
  if (!Array.isArray(value)) return undefined;

  const methods = value.flatMap(item => {
    if (!isRecord(item)) return [];

    const method = readString(item.method, 300);
    if (!method) return [];

    const instructions = cleanInstructions(readStringArray(item.instructions, 30)) || [];
    return [{
      method,
      difficulty: readString(item.difficulty, 200) || 'medium',
      instructions,
      timeframe: readString(item.timeframe, 500) || '',
    }];
  });

  return methods.length > 0 ? methods : undefined;
}

function parseSeedResult(parsed: Record<string, unknown>): SeedIdentificationResult {
  const seedRaw = isRecord(parsed.seed) ? parsed.seed : parsed;
  const seedType = readLowercaseEnum(seedRaw.seedType, ['orthodox', 'recalcitrant', 'unknown'] as const)
    || 'unknown';
  const lightRequirement = readLowercaseEnum(
    seedRaw.lightRequirement,
    ['light', 'dark', 'either'] as const,
  );
  const comesTrueFromSeed = typeof seedRaw.comesTrueFromSeed === 'boolean'
    ? seedRaw.comesTrueFromSeed
    : 'unknown';
  const stratification = parseSeedTreatment(seedRaw.stratification, true);
  const scarification = parseSeedTreatment(seedRaw.scarification, false);
  const germinationSteps = cleanInstructions(readStringArray(seedRaw.germinationSteps, 40));
  const commonName = readKnownName(seedRaw.commonName, true);
  const scientificName = readKnownName(seedRaw.scientificName);
  const confidence = readOptionalConfidence(seedRaw.confidence);
  const comesTrueNote = readString(seedRaw.comesTrueNote, 4_000);
  const canSaveSeed = typeof seedRaw.canSaveSeed === 'boolean'
    ? seedRaw.canSaveSeed
    : undefined;
  const savingMethod = readString(seedRaw.savingMethod, 4_000);
  const storageMethod = readString(seedRaw.storageMethod, 4_000);
  const storageDuration = readString(seedRaw.storageDuration, 1_000);
  const plantingDepth = readString(seedRaw.plantingDepth, 500);
  const temperatureRange = readString(seedRaw.temperatureRange, 500);
  const moisture = readString(seedRaw.moisture, 1_000);
  const daysToGerminate = readString(seedRaw.daysToGerminate, 500);
  const germinationRate = readString(seedRaw.germinationRate, 500);
  const propagationMethods = parseSeedPropagationMethods(seedRaw.propagationMethods);
  const timeToGrow = readString(seedRaw.timeToGrow, 1_000);
  const timeToFruit = readString(seedRaw.timeToFruit, 1_000);
  const timeToHarvest = readString(seedRaw.timeToHarvest, 1_000);
  const expectedOutcome = readString(seedRaw.expectedOutcome, 2_000);
  const spaceNeeded = readString(seedRaw.spaceNeeded, 1_000);

  return {
    ...(commonName ? { commonName } : {}),
    ...(scientificName ? { scientificName } : {}),
    ...(confidence !== undefined ? { confidence } : {}),
    alternates: readStringArray(seedRaw.alternates, 3),
    seedType,
    comesTrueFromSeed,
    ...(comesTrueNote ? { comesTrueNote } : {}),
    ...(canSaveSeed !== undefined ? { canSaveSeed } : {}),
    ...(savingMethod ? { savingMethod } : {}),
    ...(storageMethod ? { storageMethod } : {}),
    ...(storageDuration ? { storageDuration } : {}),
    ...(stratification
      ? {
        stratification: {
          needed: stratification.needed,
          method: stratification.method,
          duration: stratification.duration || '',
        },
      }
      : {}),
    ...(scarification
      ? {
        scarification: {
          needed: scarification.needed,
          method: scarification.method,
        },
      }
      : {}),
    ...(plantingDepth ? { plantingDepth } : {}),
    ...(lightRequirement ? { lightRequirement } : {}),
    ...(temperatureRange ? { temperatureRange } : {}),
    ...(moisture ? { moisture } : {}),
    ...(daysToGerminate ? { daysToGerminate } : {}),
    ...(germinationRate ? { germinationRate } : {}),
    ...(germinationSteps?.length ? { germinationSteps } : {}),
    ...(propagationMethods ? { propagationMethods } : {}),
    ...(timeToGrow ? { timeToGrow } : {}),
    ...(timeToFruit ? { timeToFruit } : {}),
    ...(timeToHarvest ? { timeToHarvest } : {}),
    ...(expectedOutcome ? { expectedOutcome } : {}),
    ...(spaceNeeded ? { spaceNeeded } : {}),
  };
}

function parseIdentification(content: string): ParsedIdentification {
  const parsed = parseJsonObject(content);
  const isPlant = typeof parsed.isPlant === 'boolean' ? parsed.isPlant : undefined;
  const commonName = readKnownName(parsed.commonName, true);
  const scientificName = readKnownName(parsed.scientificName);
  const evidence = readString(parsed.evidence, 4_000);
  const uncertainty = readString(parsed.uncertainty, 4_000);
  const propagation = parsePropagation(parsed.propagation);
  const petSafety = readLowercaseEnum(
    parsed.petSafety,
    ['safe', 'caution', 'toxic', 'unknown'] as const,
  );
  const kingdom = readLowercaseEnum(
    parsed.kingdom,
    ['mushroom', 'bracket_fungus', 'slime_mold', 'lichen', 'moss', 'plant', 'other'] as const,
  );
  const edibility = readLowercaseEnum(
    parsed.edibility,
    ['edible', 'poisonous', 'unknown'] as const,
  );
  const cultivable = typeof parsed.cultivable === 'boolean' ? parsed.cultivable : undefined;
  const rawSubstrate = readString(parsed.substrate, 2_000);
  const substrate = rawSubstrate && !isUnknownLabel(rawSubstrate) ? rawSubstrate : undefined;

  return {
    ...(isPlant !== undefined ? { isPlant } : {}),
    ...(commonName ? { commonName } : {}),
    ...(scientificName ? { scientificName } : {}),
    confidence: toConfidencePercent(parsed.confidence),
    ...(evidence ? { evidence } : {}),
    ...(uncertainty ? { uncertainty } : {}),
    alternates: readStringArray(parsed.alternates, 3),
    ...(propagation ? { propagation } : {}),
    ...(petSafety ? { petSafety } : {}),
    ...(kingdom ? { kingdom } : {}),
    ...(edibility ? { edibility } : {}),
    ...(cultivable !== undefined ? { cultivable } : {}),
    ...(substrate ? { substrate } : {}),
    raw: parsed,
  };
}

function readChoice(response: XiaomiChatResponse): Record<string, unknown> | undefined {
  if (!Array.isArray(response.choices)) return undefined;
  return response.choices.find(isRecord);
}

export function readXiaomiFinishReason(response: XiaomiChatResponse): string | undefined {
  return readString(readChoice(response)?.finish_reason, 100);
}

function readMessageContent(response: XiaomiChatResponse): string | undefined {
  const choice = readChoice(response);
  const message = isRecord(choice?.message) ? choice.message : undefined;
  const content = message?.content;

  if (typeof content === 'string') return content;
  if (!Array.isArray(content)) return undefined;

  const textParts = content.flatMap(part => {
    if (typeof part === 'string') return [part];
    if (!isRecord(part)) return [];
    const text = readString(part.text, 100_000);
    return text ? [text] : [];
  });
  return textParts.length > 0 ? textParts.join('\n') : undefined;
}

function normalizePetSafety(
  value: string | undefined,
): NonNullable<PlantIdentificationResult['petSafety']> {
  return value === 'safe' || value === 'caution' || value === 'toxic'
    ? value
    : 'unknown';
}

function createSuggestion(
  commonName: string | undefined,
  scientificName: string | undefined,
  confidence: number,
  description?: string,
): PlantIdentificationSuggestion | null {
  const name = commonName || scientificName;
  if (!name) return null;

  return {
    name,
    ...(commonName ? { commonName } : {}),
    ...(scientificName ? { scientificName } : {}),
    confidence,
    commonNames: commonName ? [commonName] : [],
    ...(description ? { description } : {}),
  };
}

function createSuggestions(parsed: ParsedIdentification): PlantIdentificationSuggestion[] {
  const topSuggestion = createSuggestion(
    parsed.commonName,
    parsed.scientificName,
    parsed.confidence,
    parsed.evidence,
  );
  const usedNames = new Set<string>();

  if (topSuggestion) {
    for (const name of [topSuggestion.name, topSuggestion.commonName, topSuggestion.scientificName]) {
      if (name) usedNames.add(name.toLocaleLowerCase('en-US'));
    }
  }

  const alternates = parsed.alternates.flatMap((name, index) => {
    const key = name.toLocaleLowerCase('en-US');
    if (usedNames.has(key)) return [];
    usedNames.add(key);

    return [{
      name,
      commonName: name,
      confidence: Math.max(0, parsed.confidence - ((index + 1) * 8)),
      commonNames: [name],
    } satisfies PlantIdentificationSuggestion];
  });

  return topSuggestion ? [topSuggestion, ...alternates] : alternates;
}

export function mapXiaomiMimoResponse(
  input: PlantIdentificationInput,
  response: XiaomiChatResponse,
): PlantIdentificationResult {
  const content = readMessageContent(response);
  if (!content) {
    warnDev('[PlantIdentification] Pixie response parse failed', {
      provider: 'xiaomi-mimo',
      message: 'empty_content',
    });
    throw new PlantIdentificationError(
      'PROVIDER_UNAVAILABLE',
      'Pixie returned an empty response.',
      undefined,
      'Pixie returned HTTP 200 with empty message content.',
    );
  }

  const parsed = parseIdentification(content);
  const suggestions = createSuggestions(parsed);
  const bestSuggestion = suggestions[0];
  const isFungus = input.mode === 'fungus';
  const isSeed = input.mode === 'seed';

  if (
    (!isFungus && !isSeed && parsed.isPlant === false)
    || !bestSuggestion
    || bestSuggestion.confidence < 15
  ) {
    warnDev('[PlantIdentification] Pixie produced no confident result', {
      provider: 'xiaomi-mimo',
      suggestionCount: suggestions.length,
      topConfidence: bestSuggestion?.confidence || 0,
      isPlant: parsed.isPlant,
      mode: input.mode,
    });
    throw new PlantIdentificationError(
      'NO_CONFIDENT_RESULT',
      isFungus
        ? 'No confident fungus result was returned.'
        : isSeed
          ? 'No confident seed result was returned.'
          : 'No confident plant result was returned.',
    );
  }

  const description = [
    bestSuggestion.description,
    parsed.uncertainty ? `Uncertainty: ${parsed.uncertainty}` : null,
  ].filter(Boolean).join('\n\n') || undefined;
  const decoratedTopSuggestion: PlantIdentificationSuggestion = {
    ...bestSuggestion,
    ...(description ? { description } : {}),
  };
  const decoratedSuggestions = [decoratedTopSuggestion, ...suggestions.slice(1)];
  const imageUri = getPrimaryPlantScanImageUri(input);

  if (!imageUri) {
    throw new PlantIdentificationError('NO_IMAGE', 'A plant photo is required.');
  }

  const imageUris = getPlantScanResultImageUris(input);
  const providerRequestId = readString(response.id, 500);
  const userProvidedName = normalizeOptionalText(input.optionalKnownName);
  const nearbyCityOrZip = getPreferredApproximateLocationText(input);
  const notes = normalizeOptionalText(input.notes, 2_000);
  const seed = isSeed ? parseSeedResult(parsed.raw) : undefined;

  return {
    id: createTimestampId(
      isFungus
        ? 'xiaomi_fungus_scan_result'
        : isSeed
          ? 'xiaomi_seed_scan_result'
          : 'xiaomi_plant_scan_result',
    ),
    imageUri,
    ...(imageUris ? { imageUris } : {}),
    createdAt: new Date().toISOString(),
    provider: 'xiaomi-mimo',
    ...(providerRequestId ? { providerRequestId } : {}),
    mode: isFungus ? 'fungus' : isSeed ? 'seed' : 'plant',
    topSuggestion: decoratedTopSuggestion,
    suggestions: decoratedSuggestions,
    confidence: decoratedTopSuggestion.confidence,
    ...(decoratedTopSuggestion.scientificName
      ? { scientificName: decoratedTopSuggestion.scientificName }
      : {}),
    commonNames: decoratedTopSuggestion.commonNames,
    ...(description ? { description } : {}),
    rawResponse: response,
    ...(userProvidedName ? { userProvidedName } : {}),
    plantType: input.plantType,
    locationContext: input.locationContext,
    ...(nearbyCityOrZip ? { nearbyCityOrZip } : {}),
    ...(notes ? { notes } : {}),
    addedToGarden: false,
    addedToWishlist: false,
    ...(parsed.propagation ? { propagation: parsed.propagation } : {}),
    petSafety: normalizePetSafety(parsed.petSafety),
    ...(isFungus && parsed.kingdom ? { kingdom: parsed.kingdom } : {}),
    ...(isFungus ? { edibility: parsed.edibility || 'unknown' } : {}),
    ...(isFungus && parsed.cultivable !== undefined
      ? { cultivable: parsed.cultivable }
      : {}),
    ...(isFungus && parsed.substrate ? { substrate: parsed.substrate } : {}),
    ...(seed ? { seed } : {}),
  };
}
