/**
 * Utility functions for formula autocomplete with XBRL concepts and field references.
 */

import type { AvailableXbrlConcept } from '../types/financial';
import type { TargetField } from '../types/mappings';

/**
 * Debounce function to limit how often a function can be called.
 */
export function debounce<T extends (...args: never[]) => unknown>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: ReturnType<typeof setTimeout> | null = null;
  return (...args: Parameters<T>) => {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => func(...args), wait);
  };
}

/**
 * Result of trigger detection in a formula string.
 */
export interface TriggerInfo {
  /** The character that triggered autocomplete ('#' or '$') */
  triggerChar: '#' | '$';
  /** The range of the trigger and query in the formula */
  triggerRange: {
    start: number;
    end: number;
  };
  /** The query string typed after the trigger character */
  query: string;
}

/**
 * Suggestion item for autocomplete.
 */
export interface SuggestionItem {
  type: 'concept' | 'field';
  displayText: string;
  insertText: string;
  metadata?: {
    taxonomy?: string;
    label?: string;
    unitFamily?: string;
    fieldDisplayName?: string;
  };
}

/**
 * Type guard to check if trigger info is valid.
 */
export function isValidTriggerInfo(trigger: TriggerInfo | null): trigger is TriggerInfo {
  return trigger !== null &&
         trigger.triggerRange.start >= 0 &&
         trigger.triggerRange.end <= trigger.triggerRange.start + trigger.query.length + 1;
}

/**
 * Detects if the cursor is at a trigger position for autocomplete.
 * @param formula - The current formula string
 * @param cursorPosition - The current cursor position in the formula
 * @returns Trigger info if at a valid trigger position, null otherwise
 */
export function detectTrigger(
  formula: string,
  cursorPosition: number
): TriggerInfo | null {
  if (cursorPosition <= 0) {
    return null;
  }

  // Validate cursor position
  if (cursorPosition > formula.length) {
    return null;
  }

  // Get the character before the cursor
  const charBeforeCursor = formula[cursorPosition - 1];

  // Check for XBRL concept trigger (#)
  if (charBeforeCursor === '#') {
    // The trigger is at cursorPosition - 1, query starts after it
    const start = cursorPosition - 1;
    return {
      triggerChar: '#',
      triggerRange: { start, end: cursorPosition },
      query: '',
    };
  }

  // Check for field reference trigger ($)
  if (charBeforeCursor === '$') {
    // The trigger is at cursorPosition - 1, query starts after it
    const start = cursorPosition - 1;
    return {
      triggerChar: '$',
      triggerRange: { start, end: cursorPosition },
      query: '',
    };
  }

  // If we're in the middle of typing after a trigger, find the trigger position
  let triggerPos = -1;
  let triggerChar: '#' | '$' | null = null;

  // Search backward from cursor position for # or $
  for (let i = cursorPosition - 1; i >= 0; i--) {
    const char = formula[i];
    if (char === '#' || char === '$') {
      // Make sure there's no space between trigger and cursor
      const hasSpace = formula.substring(i + 1, cursorPosition).includes(' ');
      if (!hasSpace) {
        triggerPos = i;
        triggerChar = char;
      }
      break;
    }
    // Stop if we hit a space or operator (except for . and _ which are valid in concept names)
    if (/[+\-*/()^,\s]/.test(char)) {
      break;
    }
  }

  if (triggerPos !== -1 && triggerChar) {
    const query = formula.substring(triggerPos + 1, cursorPosition);
    return {
      triggerChar,
      triggerRange: { start: triggerPos, end: cursorPosition },
      query,
    };
  }

  return null;
}

/**
 * Scores a concept match for ranking.
 * Higher score = better match.
 */
function scoreConceptMatch(concept: AvailableXbrlConcept, query: string): number {
  const lowerQuery = query.toLowerCase();
  const conceptLower = concept.concept.toLowerCase();
  const taxonomyLower = concept.taxonomy.toLowerCase();
  const labelLower = concept.label?.toLowerCase() || '';

  // Exact match on concept name (highest priority)
  if (conceptLower === lowerQuery) {
    return 1000;
  }

  // Exact match on taxonomy:concept
  const fullName = `${concept.taxonomy}:${concept.concept}`.toLowerCase();
  if (fullName === lowerQuery) {
    return 900;
  }

  // Prefix match on concept name
  if (conceptLower.startsWith(lowerQuery)) {
    return 800;
  }

  // Prefix match on taxonomy
  if (taxonomyLower.startsWith(lowerQuery)) {
    return 700;
  }

  // Concept name contains query
  if (conceptLower.includes(lowerQuery)) {
    return 600;
  }

  // Label contains query
  if (labelLower.includes(lowerQuery)) {
    return 500;
  }

  // Taxonomy contains query
  if (taxonomyLower.includes(lowerQuery)) {
    return 400;
  }

  // Full name contains query
  if (fullName.includes(lowerQuery)) {
    return 300;
  }

  return 0;
}

/**
 * Filters and ranks XBRL concepts based on a query string.
 * @param concepts - All available concepts
 * @param query - The search query
 * @returns Filtered and sorted concepts (max 20)
 */
export function filterConcepts(
  concepts: AvailableXbrlConcept[],
  query: string
): AvailableXbrlConcept[] {
  if (!query) {
    // Return first 20 concepts when no query
    return concepts.slice(0, 20);
  }

  // Score and filter concepts
  const scored = concepts
    .map((concept) => ({
      concept,
      score: scoreConceptMatch(concept, query),
    }))
    .filter((item) => item.score > 0)
    .sort((a, b) => {
      // Sort by score (descending), then by taxonomy, then by concept name
      if (b.score !== a.score) {
        return b.score - a.score;
      }
      if (a.concept.taxonomy !== b.concept.taxonomy) {
        return a.concept.taxonomy.localeCompare(b.concept.taxonomy);
      }
      return a.concept.concept.localeCompare(b.concept.concept);
    });

  return scored.slice(0, 20).map((item) => item.concept);
}

/**
 * Scores a field match for ranking.
 */
function scoreFieldMatch(field: TargetField, query: string): number {
  const lowerQuery = query.toLowerCase();
  const fieldLower = field.toLowerCase();

  // Exact match
  if (fieldLower === lowerQuery) {
    return 1000;
  }

  // Prefix match
  if (fieldLower.startsWith(lowerQuery)) {
    return 800;
  }

  // Contains match
  if (fieldLower.includes(lowerQuery)) {
    return 600;
  }

  return 0;
}

/**
 * Filters target fields based on a query string.
 * @param fields - All available target fields
 * @param query - The search query
 * @returns Filtered and sorted fields
 */
export function filterFields(
  fields: TargetField[],
  query: string
): TargetField[] {
  if (!query) {
    return fields;
  }

  const scored = fields
    .map((field) => ({
      field,
      score: scoreFieldMatch(field, query),
    }))
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score);

  return scored.map((item) => item.field);
}

/**
 * Converts a concept to a suggestion item.
 */
export function conceptToSuggestion(concept: AvailableXbrlConcept): SuggestionItem {
  return {
    type: 'concept',
    displayText: `${concept.taxonomy}:${concept.concept}`,
    insertText: `#${concept.taxonomy}.${concept.concept}[${concept.unitFamily}]`,
    metadata: {
      taxonomy: concept.taxonomy,
      label: concept.label,
      unitFamily: concept.unitFamily,
    },
  };
}

/**
 * Converts a field to a suggestion item.
 */
export function fieldToSuggestion(
  field: TargetField,
  displayNames: Record<TargetField, string>
): SuggestionItem {
  return {
    type: 'field',
    displayText: field,
    insertText: `$${field}`,
    metadata: {
      fieldDisplayName: displayNames[field],
    },
  };
}

/**
 * Inserts a completion into the formula at the trigger position.
 * @param formula - The current formula string
 * @param triggerRange - The range of the trigger and query
 * @param completion - The text to insert (without trigger character)
 * @returns The updated formula string
 */
export function insertCompletion(
  formula: string,
  triggerRange: { start: number; end: number },
  completion: string
): string {
  // Validate inputs
  if (triggerRange.start < 0 || triggerRange.end > formula.length || triggerRange.start > triggerRange.end) {
    throw new Error('Invalid trigger range');
  }

  const before = formula.substring(0, triggerRange.start);
  const after = formula.substring(triggerRange.end);
  return `${before}${completion}${after}`;
}

/**
 * Gets cursor position in pixels for positioning the autocomplete popup.
 * @param textarea - The textarea element
 * @returns The position {top, left} in pixels, or null if unable to calculate
 */
export function getCursorPosition(
  textarea: HTMLTextAreaElement
): { top: number; left: number } | null {
  try {
    const { selectionStart } = textarea;

    if (selectionStart === null || selectionStart === undefined) {
      return null;
    }

    // Validate that the textarea is in the DOM
    if (!document.body.contains(textarea)) {
      return null;
    }

    // Create a mirror div to measure text position
    const mirror = document.createElement('div');
    const style = window.getComputedStyle(textarea);

    // Copy all relevant styles
    const properties: (keyof CSSStyleDeclaration)[] = [
      'direction',
      'boxSizing',
      'width',
      'height',
      'overflowX',
      'overflowY',
      'borderTopWidth',
      'borderRightWidth',
      'borderBottomWidth',
      'borderLeftWidth',
      'paddingTop',
      'paddingRight',
      'paddingBottom',
      'paddingLeft',
      'fontStyle',
      'fontVariant',
      'fontWeight',
      'fontStretch',
      'fontSize',
      'lineHeight',
      'fontFamily',
      'textAlign',
      'textTransform',
      'textIndent',
      'textDecoration',
      'letterSpacing',
      'wordSpacing',
    ];

    properties.forEach((prop) => {
      const value = style[prop];
      if (value && typeof value === 'string') {
        try {
          mirror.style.setProperty(String(prop), value);
        } catch {
          // Some CSS properties can't be set directly, ignore them
        }
      }
    });

    // Position mirror off-screen
    mirror.style.position = 'absolute';
    mirror.style.visibility = 'hidden';
    mirror.style.whiteSpace = 'pre-wrap';
    mirror.style.wordWrap = 'break-word';

    // Copy text up to cursor position
    const textBeforeCursor = textarea.value.substring(0, selectionStart);
    mirror.textContent = textBeforeCursor;

    document.body.appendChild(mirror);

    const rect = textarea.getBoundingClientRect();
    const mirrorRect = mirror.getBoundingClientRect();

    // Clean up mirror element
    document.body.removeChild(mirror);

    // Calculate position, ensuring the popup stays within viewport bounds
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;

    let top = rect.top + mirrorRect.height + scrollTop + 4;
    let left = rect.left + mirrorRect.width + scrollLeft;

    // Ensure popup doesn't go off the right edge of the viewport
    const viewportWidth = window.innerWidth;
    if (left + 300 > viewportWidth) { // 300 is approximate popup width
      left = viewportWidth - 310; // Keep some padding
    }

    // Ensure popup doesn't go off the bottom edge of the viewport
    const viewportHeight = window.innerHeight;
    if (top + 280 > viewportHeight) { // 280 is approximate popup height
      top = rect.top + scrollTop - 280; // Show above the textarea
    }

    return { top, left };
  } catch (error) {
    console.warn('Error calculating cursor position:', error);
    return null;
  }
}

/**
 * Gets position below the textarea for positioning the autocomplete popup.
 * @param textarea - The textarea element
 * @returns The position {top, left} in pixels, or null if unable to calculate
 */
export function getTextareaPosition(
  textarea: HTMLTextAreaElement
): { top: number; left: number } | null {
  try {
    // Validate that the textarea is in the DOM
    if (!document.body.contains(textarea)) {
      return null;
    }

    const rect = textarea.getBoundingClientRect();

    // Calculate position below the textarea
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;

    // Position below the textarea with a small gap
    const top = rect.bottom + scrollTop + 4;
    const left = rect.left + scrollLeft;

    return { top, left };
  } catch (error) {
    console.warn('Error calculating textarea position:', error);
    return null;
  }
}
