import { Phenotype, RarityTier } from '@ranchlands/shared';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface Genotype {
  gWeight: number;
  gMilkYield: number;
  gGrowthRate: number;
  gTemperament: number;
  gCoatQuality: number;
  gFertility: number;
  gHardiness: number;
  gShowPotential: number;
}

export interface BreedResult {
  genotype: Genotype;
  phenotype: Phenotype;
  rarityTier: RarityTier;
  inbreedingCoefficient: number;
}

export interface OffspringPreviewStats {
  min: number;
  max: number;
  mean: number;
}

export type TraitPreview = Record<keyof Phenotype, OffspringPreviewStats>;

// ─── Gaussian noise (Box-Muller transform) ────────────────────────────────────

function gaussianNoise(sigma: number): number {
  let u = 0, v = 0;
  while (u === 0) u = Math.random();
  while (v === 0) v = Math.random();
  const noise = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
  return noise * sigma;
}

function clamp(value: number, min = 0, max = 1): number {
  return Math.max(min, Math.min(max, value));
}

// ─── Genotype → Phenotype conversion ─────────────────────────────────────────
// Phenotype is derived once at birth with a small developmental noise term.
// After birth, phenotype is fixed — only health/condition change.

export function genotypeToPhenotype(genotype: Genotype): Phenotype {
  const DEV_NOISE_SIGMA = 0.04; // Developmental noise

  return {
    weightScore:       clamp(genotype.gWeight       + gaussianNoise(DEV_NOISE_SIGMA)),
    milkYieldScore:    clamp(genotype.gMilkYield    + gaussianNoise(DEV_NOISE_SIGMA)),
    growthRateScore:   clamp(genotype.gGrowthRate   + gaussianNoise(DEV_NOISE_SIGMA)),
    temperamentScore:  clamp(genotype.gTemperament  + gaussianNoise(DEV_NOISE_SIGMA)),
    coatQualityScore:  clamp(genotype.gCoatQuality  + gaussianNoise(DEV_NOISE_SIGMA)),
    fertilityScore:    clamp(genotype.gFertility    + gaussianNoise(DEV_NOISE_SIGMA)),
    hardinessScore:    clamp(genotype.gHardiness    + gaussianNoise(DEV_NOISE_SIGMA)),
    showPotentialScore:clamp(genotype.gShowPotential + gaussianNoise(DEV_NOISE_SIGMA)),
  };
}

// ─── Rarity computation ───────────────────────────────────────────────────────
// Rarity is determined by the GENOTYPE sum, not phenotype.
// This ensures rarity reflects true genetic potential.

export function computeRarity(genotype: Genotype): RarityTier {
  const values = Object.values(genotype) as number[];
  const mean = values.reduce((a, b) => a + b, 0) / values.length;
  const min = Math.min(...values);

  // Must have high mean AND no terrible traits to be rare
  if (mean >= 0.82 && min >= 0.55) return RarityTier.LEGENDARY;
  if (mean >= 0.70 && min >= 0.40) return RarityTier.RARE;
  if (mean >= 0.55 && min >= 0.25) return RarityTier.UNCOMMON;
  return RarityTier.COMMON;
}

// ─── Core breeding algorithm ──────────────────────────────────────────────────

export function breedGenotypes(
  sire: Genotype,
  dam: Genotype,
  inbreedingCoeff: number
): Genotype {
  const INHERITANCE_NOISE_SIGMA = 0.03;
  const MUTATION_CHANCE = 0.02;        // 2% per trait
  const MUTATION_MAX_MAGNITUDE = 0.15; // Max ±15% above parent range

  function inheritTrait(sireVal: number, damVal: number): number {
    // Weight is drawn uniformly [0.3, 0.7] per trait — models Mendelian inheritance
    const weight = 0.3 + Math.random() * 0.4;
    let value = sireVal * weight + damVal * (1 - weight);

    // Inheritance noise
    value += gaussianNoise(INHERITANCE_NOISE_SIGMA);

    // Mutation check
    if (Math.random() < MUTATION_CHANCE) {
      const parentRange = Math.abs(sireVal - damVal);
      const magnitude = Math.random() * Math.min(parentRange + MUTATION_MAX_MAGNITUDE, MUTATION_MAX_MAGNITUDE);
      value += Math.random() < 0.5 ? magnitude : -magnitude;
    }

    // Inbreeding can fix traits (slight boost if coeff is in the "controlled" range)
    // or harm them (high inbreeding)
    if (inbreedingCoeff > 0.25) {
      value -= gaussianNoise(0.05); // Inbreeding depression
    } else if (inbreedingCoeff > 0.05 && inbreedingCoeff <= 0.15) {
      value += 0.02; // Line-breeding bonus: slight trait fixation
    }

    return clamp(value);
  }

  return {
    gWeight:        inheritTrait(sire.gWeight, dam.gWeight),
    gMilkYield:     inheritTrait(sire.gMilkYield, dam.gMilkYield),
    gGrowthRate:    inheritTrait(sire.gGrowthRate, dam.gGrowthRate),
    gTemperament:   inheritTrait(sire.gTemperament, dam.gTemperament),
    gCoatQuality:   inheritTrait(sire.gCoatQuality, dam.gCoatQuality),
    gFertility:     inheritTrait(sire.gFertility, dam.gFertility),
    gHardiness:     inheritTrait(sire.gHardiness, dam.gHardiness),
    gShowPotential: inheritTrait(sire.gShowPotential, dam.gShowPotential),
  };
}

// ─── Monte Carlo breed preview ────────────────────────────────────────────────
// Runs N simulations to give players probability ranges WITHOUT revealing genotypes.

export function generateBreedPreview(
  sire: Genotype,
  dam: Genotype,
  inbreedingCoeff: number,
  samples = 5000
): {
  traits: TraitPreview;
  rarityDistribution: Record<RarityTier, number>;
} {
  const results: Genotype[] = [];
  const rarityCounts: Record<RarityTier, number> = {
    [RarityTier.COMMON]: 0,
    [RarityTier.UNCOMMON]: 0,
    [RarityTier.RARE]: 0,
    [RarityTier.LEGENDARY]: 0,
  };

  for (let i = 0; i < samples; i++) {
    const offspring = breedGenotypes(sire, dam, inbreedingCoeff);
    const phenotype = genotypeToPhenotype(offspring);
    results.push(offspring);
    rarityCounts[computeRarity(offspring)]++;
  }

  // Compute stats for each PHENOTYPE trait (not genotype — players see phenotype)
  const phenotypes = results.map(g => genotypeToPhenotype(g));

  function traitStats(key: keyof Phenotype): OffspringPreviewStats {
    const values = phenotypes.map(p => p[key]);
    return {
      min: Math.min(...values),
      max: Math.max(...values),
      mean: values.reduce((a, b) => a + b, 0) / values.length,
    };
  }

  return {
    traits: {
      weightScore:        traitStats('weightScore'),
      milkYieldScore:     traitStats('milkYieldScore'),
      growthRateScore:    traitStats('growthRateScore'),
      temperamentScore:   traitStats('temperamentScore'),
      coatQualityScore:   traitStats('coatQualityScore'),
      fertilityScore:     traitStats('fertilityScore'),
      hardinessScore:     traitStats('hardinessScore'),
      showPotentialScore: traitStats('showPotentialScore'),
    },
    rarityDistribution: {
      [RarityTier.COMMON]:    rarityCounts[RarityTier.COMMON] / samples,
      [RarityTier.UNCOMMON]:  rarityCounts[RarityTier.UNCOMMON] / samples,
      [RarityTier.RARE]:      rarityCounts[RarityTier.RARE] / samples,
      [RarityTier.LEGENDARY]: rarityCounts[RarityTier.LEGENDARY] / samples,
    },
  };
}

// ─── Wright's inbreeding coefficient (simplified 5-gen) ───────────────────────
// Expects an ancestor map: { animalId: Set<ancestorId> }

export function computeInbreedingCoefficient(
  sireAncestors: Map<string, number>, // ancestorId -> generation
  damAncestors: Map<string, number>
): number {
  let coefficient = 0;

  for (const [ancestorId, sireGen] of sireAncestors) {
    if (damAncestors.has(ancestorId)) {
      const damGen = damAncestors.get(ancestorId)!;
      // Wright's path coefficient contribution: (0.5)^(n1 + n2 + 1)
      coefficient += Math.pow(0.5, sireGen + damGen + 1);
    }
  }

  return Math.min(coefficient, 1); // Cap at 1
}

// ─── Show scoring ─────────────────────────────────────────────────────────────

export function computeShowScore(
  phenotype: Phenotype,
  condition: number,
  showId: string,
  animalId: string,
  hasPrepBarn = false
): number {
  // Deterministic seeded noise for auditable results
  const seed = hashString(showId + animalId);
  const seededRandom = seededRandomFromHash(seed);
  const noise = 1 + (seededRandom - 0.5) * 0.16; // ±8%

  const traitSum = Object.values(phenotype).reduce((a, b) => a + b, 0);
  const traitMean = traitSum / 8; // Normalize to 0-1

  const conditionFactor = condition / 100;
  const prepBonusFactor = hasPrepBarn ? 1.03 : 1.0;

  return parseFloat(
    (traitMean * 100 * conditionFactor * prepBonusFactor * noise).toFixed(4)
  );
}

function hashString(str: string): number {
  let hash = 5381;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
  }
  return Math.abs(hash);
}

function seededRandomFromHash(hash: number): number {
  // Simple LCG from seed
  const a = 1664525;
  const c = 1013904223;
  const m = Math.pow(2, 32);
  return ((a * hash + c) % m) / m;
}

// ─── Random starter animal ────────────────────────────────────────────────────

export function generateStarterGenotype(): Genotype {
  // Starter animals are below-average to give room for progression
  function starterTrait(): number {
    return clamp(0.25 + Math.random() * 0.35); // Range: 0.25-0.60
  }

  return {
    gWeight:        starterTrait(),
    gMilkYield:     starterTrait(),
    gGrowthRate:    starterTrait(),
    gTemperament:   starterTrait(),
    gCoatQuality:   starterTrait(),
    gFertility:     starterTrait(),
    gHardiness:     starterTrait(),
    gShowPotential: starterTrait(),
  };
}
