import { Tone } from '../types';
import type { ExceptionRule } from './exceptionService';
import type { StructuredFields } from '../utils/structuredFields';

export const SITE_CONTENT_RESOURCE_TYPE = 'site_content' as const;
export const SITE_CONTENT_CONTRACT_VERSION = 1;
export const SITE_CONTENT_MAX_PAYLOAD_BYTES = 1_048_576;
export const SITE_CONTENT_MAX_SEGMENTS = 1_000;
export const SITE_CONTENT_MAX_TEXT_CHARACTERS = 1_048_576;
const SITE_CONTENT_PAYLOAD_PREFIX = 'IPZ_SITE_CONTENT_V1:';
const SEGMENT_ID_PATTERN = /^seg_[A-Za-z0-9_-]{32}$/;
const REVISION_PATTERN = /^[a-f0-9]{64}$/;
const LANGUAGE_PATTERN = /^[a-z]{2,3}(?:-[a-z0-9]{2,6})?$/;
const MAX_CONTEXT_DEPTH = 20;
const MAX_CONTEXT_NODES = 10_000;

export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
export type JsonObject = { [key: string]: JsonValue };

export interface SiteContentSegment {
  id: string;
  text: string;
  context: JsonObject;
}

export interface SiteContentJobSubmitRequest {
  resourceType: typeof SITE_CONTENT_RESOURCE_TYPE;
  contractVersion: typeof SITE_CONTENT_CONTRACT_VERSION;
  sourceLang: string;
  targetLang: string;
  sourceRevision: string;
  attemptToken: string;
  segments: SiteContentSegment[];
  tone?: Tone;
  exceptions?: ExceptionRule[];
  callbackUrl: string;
  callbackSecret: string;
  clientJobId: string;
}

interface StructuredProviderResult {
  translatedFields: StructuredFields;
  tokens_used: number;
  input_tokens: number;
  output_tokens: number;
  processing_time_ms: number;
  model_used: string;
}

export interface SiteContentTranslationResult extends Omit<StructuredProviderResult, 'translatedFields'> {
  translation: string;
}

const requestKeys = new Set([
  'resourceType',
  'contractVersion',
  'sourceLang',
  'targetLang',
  'sourceRevision',
  'attemptToken',
  'segments',
  'tone',
  'exceptions',
  'callbackUrl',
  'callbackSecret',
  'clientJobId',
]);
const segmentKeys = new Set(['id', 'text', 'context']);
const exceptionRuleKeys = new Set(['text', 'match_type']);

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function hasOnlyKeys(value: Record<string, unknown>, allowed: Set<string>): boolean {
  return Object.keys(value).every((key) => allowed.has(key));
}

function countCharacters(value: string): number {
  return Array.from(value).length;
}

function validateContext(context: unknown): asserts context is JsonObject {
  if (!isPlainObject(context)) throw new Error('Site Content segment context must be an object.');

  const pending: Array<{ value: unknown; depth: number }> = [{ value: context, depth: 0 }];
  let nodes = 0;
  while (pending.length > 0) {
    const current = pending.pop()!;
    nodes += 1;
    if (nodes > MAX_CONTEXT_NODES || current.depth > MAX_CONTEXT_DEPTH) {
      throw new Error('Site Content segment context exceeds supported limits.');
    }
    if (current.value === null || typeof current.value === 'boolean' || typeof current.value === 'string') continue;
    if (typeof current.value === 'number') {
      if (!Number.isFinite(current.value)) throw new Error('Site Content segment context contains an invalid number.');
      continue;
    }
    if (Array.isArray(current.value)) {
      for (const child of current.value) pending.push({ value: child, depth: current.depth + 1 });
      continue;
    }
    if (!isPlainObject(current.value)) throw new Error('Site Content segment context is not valid JSON.');
    for (const [key, child] of Object.entries(current.value)) {
      const invalidControl = Array.from(key).some((character) => {
        const codePoint = character.codePointAt(0) ?? 0;
        return codePoint <= 0x1f || codePoint === 0x7f;
      });
      if (key.length < 1 || key.length > 128 || invalidControl) {
        throw new Error('Site Content segment context contains an invalid key.');
      }
      pending.push({ value: child, depth: current.depth + 1 });
    }
  }
}

function encodedBytes(value: unknown): number {
  let encoded: string | undefined;
  try {
    encoded = JSON.stringify(value);
  } catch {
    throw new Error('Site Content payload is not JSON serializable.');
  }
  if (encoded === undefined) throw new Error('Site Content payload is not JSON serializable.');
  return Buffer.byteLength(encoded, 'utf8');
}

export function isSiteContentRequest(value: unknown): value is SiteContentJobSubmitRequest {
  return isPlainObject(value) && value.resourceType === SITE_CONTENT_RESOURCE_TYPE;
}

export function validateSiteContentRequest(
  value: unknown,
  maxCharacters: number
): SiteContentJobSubmitRequest {
  if (!Number.isSafeInteger(maxCharacters) || maxCharacters < 1) {
    throw new Error('Invalid Site Content character limit.');
  }
  const effectiveCharacterLimit = Math.min(maxCharacters, SITE_CONTENT_MAX_TEXT_CHARACTERS);
  if (!isPlainObject(value) || value.resourceType !== SITE_CONTENT_RESOURCE_TYPE || !hasOnlyKeys(value, requestKeys)) {
    throw new Error('Invalid segmented Site Content request.');
  }
  if (value.contractVersion !== SITE_CONTENT_CONTRACT_VERSION) {
    throw new Error('Unsupported Site Content contract version.');
  }
  if (typeof value.sourceLang !== 'string' || typeof value.targetLang !== 'string' ||
      !LANGUAGE_PATTERN.test(value.sourceLang) || !LANGUAGE_PATTERN.test(value.targetLang)) {
    throw new Error('Invalid Site Content language.');
  }
  if (typeof value.sourceRevision !== 'string' || !REVISION_PATTERN.test(value.sourceRevision)) {
    throw new Error('Invalid Site Content source revision.');
  }
  if (typeof value.attemptToken !== 'string' || !REVISION_PATTERN.test(value.attemptToken)) {
    throw new Error('Invalid Site Content attempt token.');
  }
  if (!Array.isArray(value.segments) || value.segments.length < 1 || value.segments.length > SITE_CONTENT_MAX_SEGMENTS) {
    throw new Error(`Site Content requests require 1 to ${SITE_CONTENT_MAX_SEGMENTS.toLocaleString('en-US')} segments.`);
  }
  if (typeof value.callbackUrl !== 'string' || value.callbackUrl.length < 1 ||
      typeof value.callbackSecret !== 'string' || value.callbackSecret.length < 16 || value.callbackSecret.length > 256 ||
      typeof value.clientJobId !== 'string' || value.clientJobId.length < 1 || value.clientJobId.length > 255) {
    throw new Error('Invalid Site Content callback identity.');
  }
  try {
    const callbackUrl = new URL(value.callbackUrl);
    if (callbackUrl.protocol !== 'https:' && callbackUrl.protocol !== 'http:') {
      throw new Error('unsupported protocol');
    }
  } catch {
    throw new Error('Invalid Site Content callback URL.');
  }
  if (value.tone !== undefined && !Object.values(Tone).includes(value.tone as Tone)) {
    throw new Error('Invalid Site Content tone.');
  }
  if (value.exceptions !== undefined) {
    if (!Array.isArray(value.exceptions) || value.exceptions.length > 5_000 || value.exceptions.some((rule) => {
      if (!isPlainObject(rule) || !hasOnlyKeys(rule, exceptionRuleKeys)) return true;
      return typeof rule.text !== 'string' || rule.text.trim().length < 1 || countCharacters(rule.text) > 500 ||
        (rule.match_type !== 'exact' && rule.match_type !== 'contains');
    })) {
      throw new Error('Invalid Site Content exception rules.');
    }
  }

  const seen = new Set<string>();
  let characters = 0;
  for (const segment of value.segments) {
    if (!isPlainObject(segment) || !hasOnlyKeys(segment, segmentKeys) ||
        typeof segment.id !== 'string' || !SEGMENT_ID_PATTERN.test(segment.id) || seen.has(segment.id) ||
        typeof segment.text !== 'string' || countCharacters(segment.text) < 1) {
      throw new Error('Invalid Site Content segment.');
    }
    validateContext(segment.context);
    seen.add(segment.id);
    characters += countCharacters(segment.text);
    if (characters > effectiveCharacterLimit) throw new Error('Site Content text exceeds the configured character limit.');
  }
  if (encodedBytes(value) > SITE_CONTENT_MAX_PAYLOAD_BYTES) {
    throw new Error('Site Content payload exceeds the supported payload limit.');
  }

  return value as unknown as SiteContentJobSubmitRequest;
}

export function countSiteContentCharacters(request: SiteContentJobSubmitRequest): number {
  return request.segments.reduce((total, segment) => total + countCharacters(segment.text), 0);
}

export function serializeSiteContentRequest(request: SiteContentJobSubmitRequest): string {
  const validated = validateSiteContentRequest(request, SITE_CONTENT_MAX_TEXT_CHARACTERS);
  return `${SITE_CONTENT_PAYLOAD_PREFIX}${Buffer.from(JSON.stringify(validated), 'utf8').toString('base64')}`;
}

export function parseSerializedSiteContentRequest(raw: string | null): SiteContentJobSubmitRequest | undefined {
  if (!raw?.startsWith(SITE_CONTENT_PAYLOAD_PREFIX)) return undefined;
  try {
    const encoded = raw.slice(SITE_CONTENT_PAYLOAD_PREFIX.length);
    const maxEncodedBytes = Math.ceil(SITE_CONTENT_MAX_PAYLOAD_BYTES / 3) * 4;
    if (
      encoded.length < 1 ||
      encoded.length > maxEncodedBytes ||
      encoded.length % 4 !== 0 ||
      !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)
    ) {
      throw new Error('invalid encoding');
    }
    const buffer = Buffer.from(encoded, 'base64');
    if (buffer.toString('base64') !== encoded) throw new Error('non-canonical encoding');
    return validateSiteContentRequest(JSON.parse(buffer.toString('utf8')), SITE_CONTENT_MAX_TEXT_CHARACTERS);
  } catch {
    throw new Error('Invalid stored Site Content translation payload.');
  }
}

function providerFields(request: SiteContentJobSubmitRequest): StructuredFields {
  return Object.fromEntries(
    request.segments.map((segment, index) => [`segment_${index.toString().padStart(4, '0')}`, segment.text])
  );
}

export async function translateSiteContentResource(
  request: SiteContentJobSubmitRequest,
  translate: (_fields: StructuredFields) => Promise<StructuredProviderResult>
): Promise<SiteContentTranslationResult> {
  validateSiteContentRequest(request, SITE_CONTENT_MAX_TEXT_CHARACTERS);
  const fields = providerFields(request);
  const provider = await translate(fields);
  if (!isPlainObject(provider.translatedFields)) throw new Error('Site Content provider output is invalid.');
  const metrics = [
    provider.tokens_used,
    provider.input_tokens,
    provider.output_tokens,
    provider.processing_time_ms,
  ];
  if (
    metrics.some((metric) => typeof metric !== 'number' || !Number.isFinite(metric) || metric < 0) ||
    typeof provider.model_used !== 'string' ||
    provider.model_used.length < 1
  ) {
    throw new Error('Site Content provider metrics are invalid.');
  }

  const expectedKeys = Object.keys(fields);
  const outputKeys = Object.keys(provider.translatedFields);
  if (expectedKeys.length !== outputKeys.length || expectedKeys.some((key) => !outputKeys.includes(key))) {
    throw new Error('Site Content provider output does not match the requested segments.');
  }

  const segments = request.segments.map((segment, index) => {
    const translated = provider.translatedFields[expectedKeys[index]];
    if (typeof translated !== 'string' || translated.length === 0) {
      throw new Error('Site Content provider output must contain non-empty string values.');
    }
    return { id: segment.id, text: translated, context: segment.context };
  });
  const response = {
    resource_type: SITE_CONTENT_RESOURCE_TYPE,
    source_language: request.sourceLang,
    target_language: request.targetLang,
    segments,
  };
  if (encodedBytes(response) > SITE_CONTENT_MAX_PAYLOAD_BYTES ||
      segments.reduce((total, segment) => total + countCharacters(segment.text), 0) > SITE_CONTENT_MAX_TEXT_CHARACTERS) {
    throw new Error('Site Content provider output exceeds supported limits.');
  }

  return {
    translation: JSON.stringify(response),
    tokens_used: provider.tokens_used,
    input_tokens: provider.input_tokens,
    output_tokens: provider.output_tokens,
    processing_time_ms: provider.processing_time_ms,
    model_used: provider.model_used,
  };
}

export function siteContentCallbackMetadata(request: SiteContentJobSubmitRequest): {
  resourceType: typeof SITE_CONTENT_RESOURCE_TYPE;
  contractVersion: number;
  sourceRevision: string;
  targetLang: string;
  attemptToken: string;
} {
  return {
    resourceType: SITE_CONTENT_RESOURCE_TYPE,
    contractVersion: SITE_CONTENT_CONTRACT_VERSION,
    sourceRevision: request.sourceRevision,
    targetLang: request.targetLang,
    attemptToken: request.attemptToken,
  };
}

export function siteContentPollMetadata(request: SiteContentJobSubmitRequest): {
  resourceType: typeof SITE_CONTENT_RESOURCE_TYPE;
  contractVersion: number;
  sourceRevision: string;
} {
  return {
    resourceType: SITE_CONTENT_RESOURCE_TYPE,
    contractVersion: SITE_CONTENT_CONTRACT_VERSION,
    sourceRevision: request.sourceRevision,
  };
}

export function siteContentCapability(maxCharacters: number): {
  version: number;
  resourceType: typeof SITE_CONTENT_RESOURCE_TYPE;
  maxPayloadBytes: number;
  maxSegments: number;
  maxCharacters: number;
} {
  if (!Number.isSafeInteger(maxCharacters) || maxCharacters < 1) {
    throw new Error('Invalid Site Content character limit.');
  }
  return {
    version: SITE_CONTENT_CONTRACT_VERSION,
    resourceType: SITE_CONTENT_RESOURCE_TYPE,
    maxPayloadBytes: SITE_CONTENT_MAX_PAYLOAD_BYTES,
    maxSegments: SITE_CONTENT_MAX_SEGMENTS,
    maxCharacters: Math.min(maxCharacters, SITE_CONTENT_MAX_TEXT_CHARACTERS),
  };
}
