import { pool } from "../db/client.js";
import { storageProvider } from "../storage/index.js";
import type { SemanticAnalysisRun } from "../analysis/semanticAnalysis.js";
import {
  createWordstatProvider,
  type WordstatCollectResult,
  type WordstatSeedInput,
  WordstatProviderUnavailableError
} from "./wordstatProvider.js";

type SeedRow = {
  id: string;
  phrase: string;
  cluster_id: string | null;
  cluster_title: string | null;
  priority: "high" | "medium" | "low" | null;
  source: "detected" | "ontology" | string;
};

type WordstatJobRow = {
  id: string;
  project_id: string;
  semantic_run_id: string | null;
  status: "queued" | "running" | "done" | "failed" | "cancelled";
  provider: string;
  mode: string;
  region: string;
  requested_phrases: string[];
  raw_result_key: string | null;
  result_count: number;
  error_message: string | null;
  started_at: Date | null;
  completed_at: Date | null;
  created_at: Date;
  updated_at: Date;
};

type WordstatResultRow = {
  id: string;
  job_id: string;
  seed_id: string | null;
  phrase: string;
  source_phrase: string | null;
  normalized_phrase: string | null;
  frequency: number | null;
  frequency_group: string | null;
  region: string | null;
  position: number | null;
  raw: Record<string, unknown>;
  created_at: Date;
};

export type WordstatEvidence = {
  latestJob: {
    id: string;
    status: WordstatJobRow["status"];
    provider: string;
    mode: string;
    region: string;
    requestedPhraseCount: number;
    rawResultKey: string | null;
    resultCount: number;
    errorMessage: string | null;
    startedAt: string | null;
    completedAt: string | null;
    createdAt: string;
    updatedAt: string;
  } | null;
  seedResults: Array<{
    seedId: string | null;
    phrase: string;
    sourcePhrase: string | null;
    frequency: number | null;
    frequencyGroup: string | null;
    region: string | null;
    relatedCount: number;
    collectedAt: string;
  }>;
  topResults: Array<{
    phrase: string;
    sourcePhrase: string | null;
    frequency: number | null;
    frequencyGroup: string | null;
    region: string | null;
  }>;
  summary: {
    collectedSeedCount: number;
    exactFrequencySeedCount: number;
    relatedOnlySeedCount: number;
    noSignalSeedCount: number;
    resultCount: number;
    totalFrequency: number;
    highFrequencyCount: number;
    midFrequencyCount: number;
    lowFrequencyCount: number;
  };
};

function toIsoDate(value: Date | null) {
  return value ? value.toISOString() : null;
}

function normalizePhrase(value: string) {
  return value.trim().replace(/\s+/g, " ").toLowerCase();
}

const RELATED_QUERY_STOP_WORDS = new Set([
  "a",
  "an",
  "and",
  "by",
  "for",
  "in",
  "of",
  "on",
  "or",
  "the",
  "to",
  "без",
  "в",
  "для",
  "и",
  "или",
  "как",
  "на",
  "о",
  "об",
  "от",
  "по",
  "при",
  "с",
  "со",
  "что",
  "это"
]);

const RELATED_QUERY_BLOCKLIST = [
  /\bbusiness\s+cats?\b/i,
  /\bcats?\b/i,
  /(^|\s)асу(\s|$)/i,
  /асу\s*тп/i,
  /расписан/i,
  /теори[яи]\s+автоматическ/i,
  /как\s+сделать\s+автоматическ\w*\s+содержан/i,
  /автоматическ\w*\s+содержан/i
];

const PRODUCT_SIGNAL_PATTERNS = [
  /\b(ai|ии|bpm|crm|api|workflow)\b/i,
  /1с/i,
  /агент/i,
  /ассистент/i,
  /автоматизац/i,
  /бизнес/i,
  /данн/i,
  /закуп/i,
  /интеграц/i,
  /интеллект/i,
  /инфраструктур/i,
  /искусствен/i,
  /облак/i,
  /операц/i,
  /оркестр/i,
  /платформ/i,
  /процесс/i,
  /проект/i,
  /разработ/i,
  /роботизац/i,
  /тендер/i,
  /цифров/i,
  /bim/i,
  /on[-\s]?premise/i
];

const AUTOMATION_SIGNAL_PATTERN = /автоматизац|роботизац|\bbpm\b|\bworkflow\b|процесс/i;

function stemToken(token: string) {
  if (/^[a-z0-9]+$/i.test(token)) {
    return token.toLowerCase();
  }

  return token
    .toLocaleLowerCase("ru-RU")
    .replace(/(иями|ями|ами|ого|его|ому|ему|ыми|ими|ых|их|ией|ией|иям|ям|ам|ях|ах|ов|ев|ей|ой|ый|ий|ая|яя|ое|ее|ые|ие|ую|юю|ом|ем|а|я|ы|и|у|ю|е|о)$/i, "");
}

function getMeaningfulTokenStems(phrase: string) {
  return new Set(
    phrase
      .toLocaleLowerCase("ru-RU")
      .replace(/ё/g, "е")
      .split(/[^a-zа-я0-9]+/i)
      .map((token) => token.trim())
      .filter((token) => token.length > 1 && !RELATED_QUERY_STOP_WORDS.has(token))
      .map(stemToken)
      .filter((token) => token.length > 1 && !RELATED_QUERY_STOP_WORDS.has(token))
  );
}

function countIntersection(left: Set<string>, right: Set<string>) {
  let count = 0;

  for (const item of left) {
    if (right.has(item)) {
      count += 1;
    }
  }

  return count;
}

function countProductSignals(phrase: string) {
  return PRODUCT_SIGNAL_PATTERNS.filter((pattern) => pattern.test(phrase)).length;
}

function shouldKeepRelatedQuery(seedPhrase: string, relatedPhrase: string) {
  const normalizedSeedPhrase = normalizePhrase(seedPhrase);
  const normalizedRelatedPhrase = normalizePhrase(relatedPhrase);

  if (!normalizedRelatedPhrase || normalizedRelatedPhrase === normalizedSeedPhrase) {
    return false;
  }

  if (RELATED_QUERY_BLOCKLIST.some((pattern) => pattern.test(normalizedRelatedPhrase))) {
    return false;
  }

  const seedTokens = getMeaningfulTokenStems(normalizedSeedPhrase);
  const relatedTokens = getMeaningfulTokenStems(normalizedRelatedPhrase);
  const overlap = countIntersection(seedTokens, relatedTokens);
  const productSignals = countProductSignals(normalizedRelatedPhrase);
  const automationBridge =
    AUTOMATION_SIGNAL_PATTERN.test(normalizedSeedPhrase) && AUTOMATION_SIGNAL_PATTERN.test(normalizedRelatedPhrase);

  if (overlap >= 2) {
    return true;
  }

  if (overlap >= 1 && productSignals >= 1) {
    return true;
  }

  if (automationBridge && productSignals >= 1) {
    return true;
  }

  return productSignals >= 2;
}

function mapJob(row: WordstatJobRow): NonNullable<WordstatEvidence["latestJob"]> {
  return {
    id: row.id,
    status: row.status,
    provider: row.provider,
    mode: row.mode,
    region: row.region,
    requestedPhraseCount: Array.isArray(row.requested_phrases) ? row.requested_phrases.length : 0,
    rawResultKey: row.raw_result_key,
    resultCount: row.result_count,
    errorMessage: row.error_message,
    startedAt: toIsoDate(row.started_at),
    completedAt: toIsoDate(row.completed_at),
    createdAt: row.created_at.toISOString(),
    updatedAt: row.updated_at.toISOString()
  };
}

function getFrequencyBucket(frequencyGroup: string | null) {
  const value = frequencyGroup?.toLowerCase();

  if (!value) {
    return "unknown";
  }

  if (["high", "vch", "вч"].includes(value)) {
    return "high";
  }

  if (["mid", "medium", "sch", "сч"].includes(value)) {
    return "mid";
  }

  if (["low", "nch", "нч"].includes(value)) {
    return "low";
  }

  return "unknown";
}

function buildEvidence(job: WordstatJobRow | null, results: WordstatResultRow[]): WordstatEvidence {
  const seedRows = results.filter((result) => result.source_phrase === result.phrase || !result.source_phrase);
  const relatedCounts = new Map<string, number>();
  const topResultByPhrase = new Map<string, WordstatResultRow>();

  for (const result of results) {
    if (result.source_phrase && result.source_phrase !== result.phrase) {
      relatedCounts.set(result.source_phrase, (relatedCounts.get(result.source_phrase) ?? 0) + 1);
    }

    if (result.frequency !== null) {
      const normalizedPhrase = result.normalized_phrase ?? normalizePhrase(result.phrase);
      const currentResult = topResultByPhrase.get(normalizedPhrase);

      if (!currentResult || (result.frequency ?? 0) > (currentResult.frequency ?? 0)) {
        topResultByPhrase.set(normalizedPhrase, result);
      }
    }
  }

  return {
    latestJob: job ? mapJob(job) : null,
    seedResults: seedRows.map((result) => ({
      seedId: result.seed_id,
      phrase: result.phrase,
      sourcePhrase: result.source_phrase,
      frequency: result.frequency,
      frequencyGroup: result.frequency_group,
      region: result.region,
      relatedCount: relatedCounts.get(result.phrase) ?? 0,
      collectedAt: result.created_at.toISOString()
    })),
    topResults: Array.from(topResultByPhrase.values())
      .sort((left, right) => (right.frequency ?? 0) - (left.frequency ?? 0))
      .slice(0, 20)
      .map((result) => ({
        phrase: result.phrase,
        sourcePhrase: result.source_phrase,
        frequency: result.frequency,
        frequencyGroup: result.frequency_group,
        region: result.region
      })),
    summary: {
      collectedSeedCount: seedRows.length,
      exactFrequencySeedCount: seedRows.filter((result) => result.frequency !== null).length,
      relatedOnlySeedCount: seedRows.filter(
        (result) => result.frequency === null && (relatedCounts.get(result.phrase) ?? 0) > 0
      ).length,
      noSignalSeedCount: seedRows.filter(
        (result) => result.frequency === null && (relatedCounts.get(result.phrase) ?? 0) === 0
      ).length,
      resultCount: results.length,
      totalFrequency: results.reduce((sum, result) => sum + (result.frequency ?? 0), 0),
      highFrequencyCount: results.filter((result) => getFrequencyBucket(result.frequency_group) === "high").length,
      midFrequencyCount: results.filter((result) => getFrequencyBucket(result.frequency_group) === "mid").length,
      lowFrequencyCount: results.filter((result) => getFrequencyBucket(result.frequency_group) === "low").length
    }
  };
}

async function ensureSeedRows(projectId: string, analysis: SemanticAnalysisRun) {
  const seedRows: SeedRow[] = [];

  for (const seed of analysis.seedCandidates.slice(0, 80)) {
    const inserted = await pool.query<SeedRow>(
      `
        insert into seeds (
          project_id,
          semantic_run_id,
          phrase,
          source,
          selected,
          reason,
          confidence,
          cluster_id,
          cluster_title,
          priority,
          updated_at
        )
        values ($1, $2, $3, $4, true, $5, $6, $7, $8, $9, now())
        on conflict do nothing
        returning id, phrase, cluster_id, cluster_title, priority, source;
      `,
      [
        projectId,
        analysis.runId,
        seed.phrase,
        seed.source,
        seed.reason,
        seed.priority === "high" ? 0.85 : seed.priority === "medium" ? 0.65 : 0.45,
        seed.clusterId,
        seed.clusterTitle,
        seed.priority
      ]
    );

    const row =
      inserted.rows[0] ??
      (
        await pool.query<SeedRow>(
          `
            select id, phrase, cluster_id, cluster_title, priority, source
            from seeds
            where project_id = $1
              and semantic_run_id = $2
              and lower(phrase) = lower($3)
              and coalesce(cluster_id, '') = coalesce($4, '')
            order by created_at asc
            limit 1;
          `,
          [projectId, analysis.runId, seed.phrase, seed.clusterId]
        )
      ).rows[0];

    if (row) {
      seedRows.push(row);
    }
  }

  return seedRows;
}

function toWordstatSeed(row: SeedRow): WordstatSeedInput {
  return {
    seedId: row.id,
    phrase: row.phrase,
    clusterId: row.cluster_id ?? "",
    clusterTitle: row.cluster_title ?? "",
    priority: row.priority ?? "medium",
    source: row.source === "detected" ? "detected" : "ontology"
  };
}

async function createJob(
  projectId: string,
  semanticRunId: string,
  provider: string,
  mode: string,
  region: string,
  seeds: WordstatSeedInput[]
) {
  const result = await pool.query<WordstatJobRow>(
    `
      insert into wordstat_jobs (
        project_id,
        semantic_run_id,
        status,
        provider,
        mode,
        region,
        requested_phrases,
        started_at,
        updated_at
      )
      values ($1, $2, 'running', $3, $4, $5, $6, now(), now())
      returning
        id,
        project_id,
        semantic_run_id,
        status,
        provider,
        mode,
        region,
        requested_phrases,
        raw_result_key,
        result_count,
        error_message,
        started_at,
        completed_at,
        created_at,
        updated_at;
    `,
    [projectId, semanticRunId, provider, mode, region, JSON.stringify(seeds.map((seed) => seed.phrase))]
  );

  return result.rows[0];
}

async function saveRawResult(projectId: string, semanticRunId: string, jobId: string, result: WordstatCollectResult) {
  const rawResultKey = `projects/${projectId}/wordstat/${semanticRunId}/${jobId}/raw.json`;

  await storageProvider.putObject({
    key: rawResultKey,
    body: JSON.stringify(result.raw, null, 2),
    contentType: "application/json"
  });

  return rawResultKey;
}

async function saveResults(jobId: string, result: WordstatCollectResult) {
  let resultCount = 0;

  for (const seedResult of result.seedResults) {
    await pool.query(
      `
        insert into wordstat_results (
          job_id,
          seed_id,
          phrase,
          source_phrase,
          normalized_phrase,
          frequency,
          frequency_group,
          region,
          position,
          raw
        )
        values ($1, $2, $3, $4, $5, $6, $7, $8, 0, $9);
      `,
      [
        jobId,
        seedResult.seedId,
        seedResult.seedPhrase,
        seedResult.seedPhrase,
        normalizePhrase(seedResult.seedPhrase),
        seedResult.frequency,
        seedResult.frequencyGroup,
        result.region,
        {
          source: "seed",
          provider: result.provider,
          mode: result.mode,
          collectedAt: result.collectedAt,
          raw: seedResult.raw
        }
      ]
    );
    resultCount += 1;

    const acceptedRelatedQueries = seedResult.relatedQueries.filter((relatedQuery) =>
      shouldKeepRelatedQuery(seedResult.seedPhrase, relatedQuery.phrase)
    );

    for (const [index, relatedQuery] of acceptedRelatedQueries.entries()) {
      await pool.query(
        `
          insert into wordstat_results (
            job_id,
            seed_id,
            phrase,
            source_phrase,
            normalized_phrase,
            frequency,
            frequency_group,
            region,
            position,
            raw
          )
          values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
        `,
        [
          jobId,
          seedResult.seedId,
          relatedQuery.phrase,
          seedResult.seedPhrase,
          normalizePhrase(relatedQuery.phrase),
          relatedQuery.frequency,
          relatedQuery.frequencyGroup,
          result.region,
          index + 1,
          {
            source: "related",
            provider: result.provider,
            mode: result.mode,
            collectedAt: result.collectedAt,
            raw: relatedQuery.raw
          }
        ]
      );
      resultCount += 1;
    }
  }

  return resultCount;
}

async function updateJobDone(jobId: string, rawResultKey: string, resultCount: number) {
  await pool.query(
    `
      update wordstat_jobs
      set status = 'done',
        raw_result_key = $2,
        result_count = $3,
        completed_at = now(),
        updated_at = now()
      where id = $1;
    `,
    [jobId, rawResultKey, resultCount]
  );
}

async function updateJobFailed(jobId: string, error: unknown) {
  const message = error instanceof Error ? error.message : "Не удалось собрать Wordstat evidence.";

  await pool.query(
    `
      update wordstat_jobs
      set status = 'failed',
        error_message = $2,
        completed_at = now(),
        updated_at = now()
      where id = $1;
    `,
    [jobId, message]
  );
}

export async function collectWordstatEvidence(projectId: string, analysis: SemanticAnalysisRun) {
  const provider = await createWordstatProvider();
  const status = provider.getStatus();

  if (!status.connected) {
    throw new WordstatProviderUnavailableError(status.message);
  }

  const seedRows = await ensureSeedRows(projectId, analysis);
  const seeds = seedRows.map(toWordstatSeed);
  const region = "ru";
  const job = await createJob(projectId, analysis.runId, status.provider, status.mode, region, seeds);

  if (!job) {
    throw new Error("Не удалось создать Wordstat job.");
  }

  try {
    const result = await provider.collect({
      projectId,
      semanticRunId: analysis.runId,
      region,
      seeds
    });
    const rawResultKey = await saveRawResult(projectId, analysis.runId, job.id, result);
    const resultCount = await saveResults(job.id, result);

    await updateJobDone(job.id, rawResultKey, resultCount);
  } catch (error) {
    await updateJobFailed(job.id, error);
    throw error;
  }

  return getLatestWordstatEvidence(projectId, analysis.runId);
}

export async function getLatestWordstatEvidence(projectId: string, semanticRunId: string | null): Promise<WordstatEvidence> {
  if (!semanticRunId) {
    return buildEvidence(null, []);
  }

  const jobResult = await pool.query<WordstatJobRow>(
    `
      select
        id,
        project_id,
        semantic_run_id,
        status,
        provider,
        mode,
        region,
        requested_phrases,
        raw_result_key,
        result_count,
        error_message,
        started_at,
        completed_at,
        created_at,
        updated_at
      from wordstat_jobs
      where project_id = $1 and semantic_run_id = $2
      order by created_at desc
      limit 1;
    `,
    [projectId, semanticRunId]
  );
  const latestJob = jobResult.rows[0] ?? null;

  if (!latestJob) {
    return buildEvidence(null, []);
  }

  const results = await pool.query<WordstatResultRow>(
    `
      select
        id,
        job_id,
        seed_id,
        phrase,
        source_phrase,
        normalized_phrase,
        frequency,
        frequency_group,
        region,
        position,
        raw,
        created_at
      from wordstat_results
      where job_id = $1
      order by position asc, phrase asc;
    `,
    [latestJob.id]
  );

  return buildEvidence(latestJob, results.rows);
}
