import {
  createSafeCredentialDebug,
  createTimestampId,
  fetchWithTimeout,
  isAbortError,
  logDev,
  readResponseText,
  truncateForLog,
  warnDev,
} from '../providerUtils';
import {
  alignInputWithOptimizedImages,
  optimizePlantScanImages,
} from './imageOptimization';
import {
  getPlantScanPhotos,
  getPlantScanResultImageUris,
  getPreferredApproximateLocationText,
  getPrimaryPlantScanImageUri,
  normalizeOptionalText,
} from './inputNormalization';
import type { PlantIdentificationProvider } from './plantIdentificationProvider';
import {
  PlantIdentificationError,
  type PlantIdentificationInput,
  type PlantIdentificationResult,
  type PlantIdentificationSuggestion,
  type PlantSuggestionTaxonomy,
} from './types';

declare const process: {
  env: {
    EXPO_PUBLIC_PLANT_ID_API_KEY?: string;
  };
};

const plantIdEndpoint = 'https://api.plant.id/v3/identification';
const plantIdDetails = ['common_names', 'url', 'description', 'taxonomy', 'image'];
const plantIdRequestUrl = `${plantIdEndpoint}?details=${plantIdDetails.join(',')}&language=en`;
const minimumSuggestionConfidence = 15;
const maximumImagesPerRequest = 5;
const requestTimeoutMs = 60_000;

type PlantIdSuggestion = {
  id?: unknown;
  name?: unknown;
  probability?: unknown;
  details?: unknown;
};

type PlantIdResponse = {
  access_token?: unknown;
  result?: {
    is_plant?: {
      binary?: unknown;
      probability?: unknown;
    };
    classification?: {
      suggestions?: unknown;
    };
  };
};

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

function getPlantIdApiKey(): { apiKey: string; source: string } {
  // Expo public env replacement expects direct dot access. Restart Metro/rebuild
  // after changing this value. Production should prefer a backend proxy so this
  // credential is not shipped in the mobile bundle.
  return {
    apiKey: (process.env.EXPO_PUBLIC_PLANT_ID_API_KEY || '').trim(),
    source: 'EXPO_PUBLIC_PLANT_ID_API_KEY',
  };
}

function readString(value: unknown): string | undefined {
  return typeof value === 'string' && value.trim() ? value.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 dedupeStrings(values: readonly string[]): string[] {
  const seen = new Set<string>();
  const result: string[] = [];

  for (const value of values) {
    const normalized = value.trim();
    const key = normalized.toLocaleLowerCase('en-US');
    if (!normalized || seen.has(key)) continue;
    seen.add(key);
    result.push(normalized);
  }

  return result;
}

function readStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) return [];
  return dedupeStrings(value.filter((item): item is string => typeof item === 'string'));
}

function readCommonNames(value: unknown): string[] {
  if (Array.isArray(value)) return readStringArray(value);
  if (!isRecord(value)) return [];

  const englishNames = value.en;
  if (Array.isArray(englishNames)) return readStringArray(englishNames);
  const singleEnglishName = readString(englishNames);
  return singleEnglishName ? [singleEnglishName] : [];
}

function readDescription(value: unknown): string | undefined {
  const directValue = readString(value);
  if (directValue) return directValue;
  if (!isRecord(value)) return undefined;

  return readString(value.value)
    || readString(value.text)
    || readString(value.description);
}

function readImageUrl(value: unknown): string | undefined {
  const directValue = readString(value);
  if (directValue) return directValue;

  if (Array.isArray(value)) {
    for (const item of value) {
      const imageUrl = readImageUrl(item);
      if (imageUrl) return imageUrl;
    }
    return undefined;
  }

  if (!isRecord(value)) return undefined;
  return readString(value.value)
    || readString(value.url)
    || readString(value.url_small);
}

function readTaxonomy(value: unknown): PlantSuggestionTaxonomy | undefined {
  if (!isRecord(value)) return undefined;

  const taxonomy = Object.entries(value).reduce<PlantSuggestionTaxonomy>((result, [key, item]) => {
    if (key === '__proto__' || key === 'prototype' || key === 'constructor') return result;
    const text = readString(item);
    if (text) result[key] = text;
    return result;
  }, {});

  return Object.keys(taxonomy).length > 0 ? taxonomy : undefined;
}

function readSourceUrl(details: Record<string, unknown>): string | undefined {
  const directUrl = readString(details.url);
  if (directUrl) return directUrl;

  const description = details.description;
  if (!isRecord(description)) return undefined;
  return readString(description.citation);
}

function mapSuggestion(value: unknown): PlantIdentificationSuggestion | null {
  if (!isRecord(value)) return null;

  const suggestion = value as PlantIdSuggestion;
  const scientificName = readString(suggestion.name);
  if (!scientificName) return null;

  const details = isRecord(suggestion.details) ? suggestion.details : {};
  const commonNames = readCommonNames(details.common_names);
  const id = readString(suggestion.id);
  const commonName = commonNames[0];
  const taxonomy = readTaxonomy(details.taxonomy);
  const description = readDescription(details.description);
  const sourceUrl = readSourceUrl(details);
  const imageUrl = readImageUrl(details.image);

  return {
    ...(id ? { id } : {}),
    name: commonName || scientificName,
    ...(commonName ? { commonName } : {}),
    scientificName,
    confidence: toConfidencePercent(suggestion.probability),
    commonNames,
    ...(taxonomy ? { taxonomy } : {}),
    ...(description ? { description } : {}),
    ...(sourceUrl ? { sourceUrl } : {}),
    ...(imageUrl ? { imageUrl } : {}),
  };
}

function readSuggestions(response: PlantIdResponse): PlantIdentificationSuggestion[] {
  const rawSuggestions = response.result?.classification?.suggestions;
  if (!Array.isArray(rawSuggestions)) return [];

  const mapped = rawSuggestions
    .map(mapSuggestion)
    .filter((suggestion): suggestion is PlantIdentificationSuggestion => suggestion !== null)
    .sort((left, right) => right.confidence - left.confidence);

  const seen = new Set<string>();
  return mapped.filter(suggestion => {
    const key = (suggestion.scientificName || suggestion.name).toLocaleLowerCase('en-US');
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function mapResponse(
  input: PlantIdentificationInput,
  response: PlantIdResponse,
): PlantIdentificationResult {
  const suggestions = readSuggestions(response);
  const topSuggestion = suggestions[0];
  const isPlant = response.result?.is_plant?.binary;

  if (!topSuggestion || isPlant === false || topSuggestion.confidence < minimumSuggestionConfidence) {
    warnDev('[PlantIdentification] request produced no confident result', {
      provider: 'plant.id',
      suggestionCount: suggestions.length,
      topConfidence: topSuggestion?.confidence || 0,
      isPlant,
    });
    throw new PlantIdentificationError(
      'NO_CONFIDENT_RESULT',
      'No confident plant result was returned.',
    );
  }

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

  const imageUris = getPlantScanResultImageUris(input);
  const providerRequestId = readString(response.access_token);
  const userProvidedName = normalizeOptionalText(input.optionalKnownName);
  const nearbyCityOrZip = getPreferredApproximateLocationText(input);
  const notes = normalizeOptionalText(input.notes, 2_000);

  return {
    id: createTimestampId('plant_scan_result'),
    imageUri,
    ...(imageUris ? { imageUris } : {}),
    createdAt: new Date().toISOString(),
    provider: 'plant.id',
    ...(providerRequestId ? { providerRequestId } : {}),
    mode: 'plant',
    topSuggestion,
    suggestions,
    confidence: topSuggestion.confidence,
    ...(topSuggestion.scientificName ? { scientificName: topSuggestion.scientificName } : {}),
    commonNames: topSuggestion.commonNames,
    ...(topSuggestion.taxonomy ? { taxonomy: topSuggestion.taxonomy } : {}),
    ...(topSuggestion.description ? { description: topSuggestion.description } : {}),
    ...(topSuggestion.sourceUrl ? { sourceUrl: topSuggestion.sourceUrl } : {}),
    rawResponse: response,
    ...(userProvidedName ? { userProvidedName } : {}),
    plantType: input.plantType,
    locationContext: input.locationContext,
    ...(nearbyCityOrZip ? { nearbyCityOrZip } : {}),
    ...(notes ? { notes } : {}),
    addedToGarden: false,
    addedToWishlist: false,
  };
}

export const plantIdProvider: PlantIdentificationProvider = {
  name: 'plant.id',
  async identifyPlant(input) {
    if (input.mode && input.mode !== 'plant') {
      throw new PlantIdentificationError(
        'PROVIDER_UNAVAILABLE',
        'Plant.id only supports plant scans.',
      );
    }

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

    const { apiKey, source } = getPlantIdApiKey();
    logDev('[PlantIdentification] Plant.id configuration', {
      envSource: source,
      ...createSafeCredentialDebug(apiKey),
    });

    if (!apiKey) {
      warnDev('[PlantIdentification] request blocked', {
        provider: 'plant.id',
        reason: 'missing_api_key',
      });
      throw new PlantIdentificationError('MISSING_API_KEY', 'Plant.id API key is missing.');
    }

    const suppliedImageCount = getPlantScanPhotos(input).length;
    if (suppliedImageCount > maximumImagesPerRequest) {
      warnDev('[PlantIdentification] extra Plant.id images omitted', {
        supplied: suppliedImageCount,
        maximum: maximumImagesPerRequest,
      });
    }

    const optimizedImages = await optimizePlantScanImages(
      input,
      'plant.id',
      maximumImagesPerRequest,
    );
    if (optimizedImages.length === 0) {
      throw new PlantIdentificationError('NO_IMAGE', 'The selected photo could not be read.');
    }

    const requestImages = optimizedImages;
    const requestInput = alignInputWithOptimizedImages(input, requestImages);

    const safeRequestDebug = {
      provider: 'plant.id',
      endpoint: plantIdEndpoint,
      method: 'POST',
      contentType: 'application/json',
      requestedDetails: plantIdDetails,
      imageCount: requestImages.length,
      imageBase64Lengths: requestImages.map(image => image.base64.length),
      ...createSafeCredentialDebug(apiKey),
    };

    let response: Response;
    try {
      logDev('[PlantIdentification] request started', safeRequestDebug);
      response = await fetchWithTimeout(
        plantIdRequestUrl,
        {
          method: 'POST',
          headers: {
            'Api-Key': apiKey,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            images: requestImages.map(image => image.base64),
          }),
        },
        requestTimeoutMs,
      );
      logDev('[PlantIdentification] response received', {
        provider: 'plant.id',
        status: response.status,
        ok: response.ok,
      });
    } catch (error) {
      const timedOut = isAbortError(error);
      warnDev('[PlantIdentification] request failed', {
        provider: 'plant.id',
        message: timedOut ? 'timeout' : 'network_failure',
      });
      throw new PlantIdentificationError(
        'NETWORK_FAILURE',
        timedOut ? 'Plant.id took too long to respond.' : 'Plant.id could not be reached.',
      );
    }

    if (!response.ok) {
      const errorBody = await readResponseText(response);
      warnDev('[PlantIdentification] request failed', {
        ...safeRequestDebug,
        status: response.status,
        message: response.status === 401 || response.status === 403
          ? 'auth_failure'
          : 'provider_unavailable',
        responseBody: truncateForLog(errorBody),
      });

      if (response.status === 401 || response.status === 403) {
        throw new PlantIdentificationError(
          'AUTH_FAILURE',
          'Plant.id rejected the API key.',
          response.status,
          `Auth failed: ${response.status}. Key detected: ${apiKey ? 'yes' : 'no'}. Provider: Plant.id.`,
        );
      }

      throw new PlantIdentificationError(
        'PROVIDER_UNAVAILABLE',
        'Plant.id is unavailable right now.',
        response.status,
      );
    }

    try {
      const payload: unknown = await response.json();
      if (!isRecord(payload)) throw new Error('Response was not a JSON object.');
      return mapResponse(requestInput, payload as PlantIdResponse);
    } catch (error) {
      if (error instanceof PlantIdentificationError) throw error;

      warnDev('[PlantIdentification] response parse failed', {
        provider: 'plant.id',
        message: error instanceof Error ? error.message : 'unexpected_response',
      });
      throw new PlantIdentificationError(
        'PROVIDER_UNAVAILABLE',
        'Plant.id returned an unexpected response.',
      );
    }
  },
};
