import { hasIdentifierAt, scanCodeLikeSource } from './utils';

export type KotlinSemanticNodeMatch = {
  kind: 'class' | 'property' | 'call' | 'member';
  name: string;
  lines: readonly number[];
};

export type KotlinPresentationSrpMatch = {
  primary_node: KotlinSemanticNodeMatch;
  related_nodes: readonly KotlinSemanticNodeMatch[];
  why: string;
  impact: string;
  expected_fix: string;
  lines: readonly number[];
};

export type KotlinConcreteDependencyDipMatch = {
  primary_node: KotlinSemanticNodeMatch;
  related_nodes: readonly KotlinSemanticNodeMatch[];
  why: string;
  impact: string;
  expected_fix: string;
  lines: readonly number[];
};

export type KotlinOpenClosedOcpMatch = {
  primary_node: KotlinSemanticNodeMatch;
  related_nodes: readonly KotlinSemanticNodeMatch[];
  why: string;
  impact: string;
  expected_fix: string;
  lines: readonly number[];
};

export type KotlinInterfaceSegregationMatch = {
  primary_node: KotlinSemanticNodeMatch;
  related_nodes: readonly KotlinSemanticNodeMatch[];
  why: string;
  impact: string;
  expected_fix: string;
  lines: readonly number[];
};

export type KotlinLiskovSubstitutionMatch = {
  primary_node: KotlinSemanticNodeMatch;
  related_nodes: readonly KotlinSemanticNodeMatch[];
  why: string;
  impact: string;
  expected_fix: string;
  lines: readonly number[];
};

type KotlinInterfaceMemberDeclaration = {
  name: string;
  line: number;
};

type KotlinInterfaceDeclaration = {
  name: string;
  line: number;
  members: readonly KotlinInterfaceMemberDeclaration[];
};

type KotlinTypeDeclaration = {
  name: string;
  line: number;
  conformances: readonly string[];
  bodyStartLine: number;
  bodyEndLine: number;
};

type KotlinResponsibilityMatch = {
  key: string;
  node: KotlinSemanticNodeMatch;
};

const kotlinQueryMemberNamePattern = /^(get|find|list|fetch|read|load|restore|refresh|is|has|can)/i;
const kotlinCommandMemberNamePattern =
  /^(create|update|delete|remove|save|insert|upsert|set|write|persist|clear|reset|sync|store)/i;

const registerKotlinResponsibility = (
  nodes: KotlinResponsibilityMatch[],
  key: string,
  kind: KotlinSemanticNodeMatch['kind'],
  name: string,
  lines: readonly number[]
): void => {
  if (lines.length === 0) {
    return;
  }
  nodes.push({ key, node: { kind, name, lines } });
};

const hasKotlinResponsibilityKeys = (
  nodes: readonly KotlinResponsibilityMatch[],
  keys: readonly string[]
): boolean => {
  const observedKeys = new Set(nodes.map((node) => node.key));
  return keys.every((key) => observedKeys.has(key));
};

const isKotlinQueryMemberName = (name: string): boolean => kotlinQueryMemberNamePattern.test(name);
const isKotlinCommandMemberName = (name: string): boolean =>
  kotlinCommandMemberNamePattern.test(name);

const stripKotlinLineForSemanticScan = (line: string): string => {
  return line
    .replace(/\/\/.*$/, '')
    .replace(/"(?:\\.|[^"\\])*"/g, '""');
};

const collectKotlinRegexLines = (source: string, regex: RegExp): readonly number[] => {
  const matches: number[] = [];
  source.split(/\r?\n/).forEach((line, index) => {
    const sanitized = stripKotlinLineForSemanticScan(line);
    if (sanitized.trimStart().startsWith('import ')) {
      return;
    }
    regex.lastIndex = 0;
    if (regex.test(sanitized)) {
      matches.push(index + 1);
    }
  });
  return matches;
};

const sortedUniqueLines = (lines: ReadonlyArray<number>): readonly number[] => {
  return Array.from(new Set(lines.filter((line) => Number.isFinite(line)).map((line) => Math.trunc(line))))
    .sort((left, right) => left - right);
};

const androidCustomSingletonObjectNamePattern =
  /(?:Repository|Service|Manager|Client|Store|DataSource|Gateway|Controller|Coordinator)$/;

const androidSafeObjectNamePattern =
  /(?:Route|Routes|Screen|Screens|Destination|Destinations|Module|Modules|Constants|Config|Keys|Theme|Colors|Typography)$/;

const isAndroidHiltModuleObjectContext = (
  lines: readonly string[],
  index: number
): boolean => {
  const context = lines
    .slice(Math.max(0, index - 4), index)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .join('\n');
  return /@(?:Module|InstallIn|Provides|Binds)\b/.test(context);
};

export const collectAndroidCustomSingletonObjectLines = (source: string): readonly number[] => {
  const lines = source.split(/\r?\n/);
  const matches: number[] = [];

  lines.forEach((line, index) => {
    const sanitized = stripKotlinLineForSemanticScan(line);
    if (sanitized.trimStart().startsWith('import ')) {
      return;
    }
    const objectMatch = sanitized.match(/^\s*object\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
    const objectName = objectMatch?.[1];
    if (!objectName) {
      return;
    }
    if (androidSafeObjectNamePattern.test(objectName) || isAndroidHiltModuleObjectContext(lines, index)) {
      return;
    }
    if (androidCustomSingletonObjectNamePattern.test(objectName)) {
      matches.push(index + 1);
    }
  });

  return sortedUniqueLines(matches);
};

export const hasAndroidCustomSingletonObjectUsage = (source: string): boolean =>
  collectAndroidCustomSingletonObjectLines(source).length > 0;

export const collectAndroidHiltInjectionWithoutEntryPointLines = (
  source: string
): readonly number[] => {
  const lines = source.split(/\r?\n/);
  const matches: number[] = [];

  for (let index = 0; index < lines.length; index += 1) {
    const sanitized = stripKotlinLineForSemanticScan(lines[index] ?? '');
    const classMatch = sanitized.match(
      /^\s*(?:class|open\s+class)\s+([A-Za-z_][A-Za-z0-9_]*)\b[^{]*(?:Activity|Fragment)\s*\(/
    );
    if (!classMatch) {
      continue;
    }

    const annotationContext = lines
      .slice(Math.max(0, index - 4), index)
      .map((line) => stripKotlinLineForSemanticScan(line))
      .join('\n');
    if (/@AndroidEntryPoint\b/.test(annotationContext)) {
      continue;
    }

    const classStartLine = index + 1;
    let braceDepth =
      countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
    for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
      const candidate = stripKotlinLineForSemanticScan(lines[cursor] ?? '');
      if (/@Inject\b\s+lateinit\s+var\b/.test(candidate)) {
        matches.push(classStartLine, cursor + 1);
      }
      braceDepth += countTokenOccurrences(candidate, '{');
      braceDepth -= countTokenOccurrences(candidate, '}');
      if (braceDepth <= 0) {
        break;
      }
    }
  }

  return sortedUniqueLines(matches);
};

export const hasAndroidHiltInjectionWithoutEntryPointUsage = (source: string): boolean =>
  collectAndroidHiltInjectionWithoutEntryPointLines(source).length > 0;

const countTokenOccurrences = (line: string, token: string): number => {
  return line.split(token).length - 1;
};

const extractBalancedCallSegment = (source: string, openParenIndex: number): string | undefined => {
  let depth = 0;
  for (let index = openParenIndex; index < source.length; index += 1) {
    const character = source[index];
    if (character === '(') {
      depth += 1;
    } else if (character === ')') {
      depth -= 1;
      if (depth === 0) {
        return source.slice(openParenIndex, index + 1);
      }
    }
  }
  return undefined;
};

const extractBalancedBlockSegment = (source: string, openBraceIndex: number): string | undefined => {
  let depth = 0;
  for (let index = openBraceIndex; index < source.length; index += 1) {
    const character = source[index];
    if (character === '{') {
      depth += 1;
    } else if (character === '}') {
      depth -= 1;
      if (depth === 0) {
        return source.slice(openBraceIndex, index + 1);
      }
    }
  }
  return undefined;
};

const normalizeKotlinWhenBranchName = (rawLabel: string): string => {
  const normalized = rawLabel.split(',')[0]?.trim() ?? rawLabel.trim();
  const withoutGuard = normalized.split(/\s+if\s+/)[0]?.trim() ?? normalized;
  return withoutGuard.match(/([A-Za-z_][A-Za-z0-9_]*)$/)?.[1] ?? withoutGuard;
};

const parseKotlinInterfaceDeclarations = (source: string): readonly KotlinInterfaceDeclaration[] => {
  const lines = source.split(/\r?\n/);
  const declarations: KotlinInterfaceDeclaration[] = [];
  const interfacePattern = /^\s*(?:fun\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/;
  const functionPattern = /^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/;
  const suspendFunctionPattern = /^\s*suspend\s+fun\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/;
  const propertyPattern = /^\s*(?:val|var)\s+([A-Za-z_][A-Za-z0-9_]*)\b/;

  for (let index = 0; index < lines.length; index += 1) {
    const sanitizedLine = stripKotlinLineForSemanticScan(lines[index] ?? '');
    const interfaceMatch = sanitizedLine.match(interfacePattern);
    if (!interfaceMatch) {
      continue;
    }

    const interfaceName = interfaceMatch[1];
    if (!interfaceName) {
      continue;
    }

    const members: KotlinInterfaceMemberDeclaration[] = [];
    let braceDepth =
      countTokenOccurrences(sanitizedLine, '{') - countTokenOccurrences(sanitizedLine, '}');

    for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
      const candidateLine = stripKotlinLineForSemanticScan(lines[cursor] ?? '');
      const functionMatch =
        candidateLine.match(suspendFunctionPattern) ?? candidateLine.match(functionPattern);
      if (functionMatch?.[1]) {
        members.push({
          name: functionMatch[1],
          line: cursor + 1,
        });
      } else {
        const propertyMatch = candidateLine.match(propertyPattern);
        if (propertyMatch?.[1]) {
          members.push({
            name: propertyMatch[1],
            line: cursor + 1,
          });
        }
      }

      braceDepth += countTokenOccurrences(candidateLine, '{');
      braceDepth -= countTokenOccurrences(candidateLine, '}');
      if (braceDepth <= 0) {
        break;
      }
    }

    declarations.push({
      name: interfaceName,
      line: index + 1,
      members,
    });
  }

  return declarations;
};

const parseKotlinTypeDeclarations = (source: string): readonly KotlinTypeDeclaration[] => {
  const lines = source.split(/\r?\n/);
  const declarations: KotlinTypeDeclaration[] = [];
  const classPattern =
    /^\s*(?:internal\s+|private\s+|public\s+)?(?:abstract\s+|open\s+|sealed\s+|data\s+|final\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)\b([^{]*)/;

  for (let index = 0; index < lines.length; index += 1) {
    const sanitizedLine = stripKotlinLineForSemanticScan(lines[index] ?? '');
    const classMatch = sanitizedLine.match(classPattern);
    if (!classMatch) {
      continue;
    }

    const typeName = classMatch[1];
    const tail = classMatch[2] ?? '';
    if (!typeName) {
      continue;
    }

    const conformanceSection = tail.includes(':') ? tail.split(':').slice(1).join(':') : '';
    const conformances = conformanceSection
      .split(',')
      .map((entry) => entry.trim())
      .filter((entry) => entry.length > 0)
      .map((entry) => entry.replace(/\(.*$/, '').trim())
      .map((entry) => entry.match(/([A-Za-z_][A-Za-z0-9_]*)$/)?.[1] ?? entry)
      .filter((entry) => entry.length > 0);

    let braceDepth =
      countTokenOccurrences(sanitizedLine, '{') - countTokenOccurrences(sanitizedLine, '}');
    const bodyStartLine = index + 1;
    let bodyEndLine = index + 1;

    for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
      const candidateLine = stripKotlinLineForSemanticScan(lines[cursor] ?? '');
      braceDepth += countTokenOccurrences(candidateLine, '{');
      braceDepth -= countTokenOccurrences(candidateLine, '}');
      bodyEndLine = cursor + 1;
      if (braceDepth <= 0) {
        break;
      }
    }

    declarations.push({
      name: typeName,
      line: index + 1,
      conformances,
      bodyStartLine,
      bodyEndLine,
    });
  }

  return declarations;
};

export const hasKotlinHardcodedBackgroundDispatcherUsage = (source: string): boolean => {
  return collectKotlinRegexLines(source, /\bDispatchers\s*\.\s*(?:IO|Default)\b/).length > 0;
};

export const hasKotlinWithContextUsage = (source: string): boolean => {
  return collectKotlinRegexLines(source, /\bwithContext\s*(?:<[^>\n]+>\s*)?\(/).length > 0;
};

export const hasKotlinLifecycleScopeUsage = (source: string): boolean => {
  return collectKotlinRegexLines(source, /\blifecycleScope\s*\./).length > 0;
};

export const hasKotlinSharedPreferencesUsage = (source: string): boolean => {
  return collectKotlinRegexLines(source, /\b(?:SharedPreferences|PreferenceManager\s*\.|getSharedPreferences\s*\()/).length > 0;
};

export const hasKotlinJUnit4Usage = (source: string): boolean => {
  return source.split(/\r?\n/).some((line) => {
    const sanitized = stripKotlinLineForSemanticScan(line);
    return (
      /\bimport\s+org\.junit\.(?:Test|Before|After|BeforeClass|AfterClass|Assert|Rule|ClassRule|runner\.RunWith|rules\.)\b/.test(sanitized) ||
      /@(?:RunWith|Rule|ClassRule)\b/.test(sanitized) ||
      /\bAssert\s*\./.test(sanitized)
    );
  });
};

export const hasKotlinProductionMockUsage = (source: string): boolean => {
  return collectKotlinRegexLines(
    source,
    /(?:\b(?:mockk|spyk|mockkObject|mockkStatic|mockkConstructor|mockito|mockitoSession|mockitoRule)\s*(?:<[^>\n]+>\s*)?\(|\bMockito\s*\.\s*(?:mock|spy)\s*\(|@(?:Mock|Spy|MockK|RelaxedMockK)\b)/
  ).length > 0;
};

export const collectKotlinHardcodedUiStringLines = (source: string): readonly number[] => {
  return source
    .split(/\r?\n/)
    .map((line, index) => ({ line, number: index + 1 }))
    .filter(({ line }) => {
      const withoutComment = line.replace(/\/\/.*$/, '');
      if (withoutComment.trimStart().startsWith('import ')) {
        return false;
      }
      return (
        /\bText\s*\(\s*"[^"$\n]+"/.test(withoutComment) ||
        /\bcontentDescription\s*=\s*"[^"$\n]+"/.test(withoutComment) ||
        /\b(?:Button|OutlinedButton|TextButton|NavigationBarItem)\s*\([^)]*text\s*=\s*"[^"$\n]+"/.test(withoutComment)
      );
    })
    .map(({ number }) => number);
};

export const hasKotlinHardcodedUiStringUsage = (source: string): boolean => {
  return collectKotlinHardcodedUiStringLines(source).length > 0;
};

export const hasKotlinSupervisorScopeUsage = (source: string): boolean => {
  return collectKotlinRegexLines(source, /\bsupervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{)/).length > 0;
};

export const hasKotlinCoroutineTryCatchUsage = (source: string): boolean => {
  const sanitizedSource = source
    .split(/\r?\n/)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .filter((line) => !line.trimStart().startsWith('import '))
    .join('\n');

  return (
    /\btry\s*\{[\s\S]*\bcatch\s*\(/.test(sanitizedSource) &&
    /\b(?:suspend\s+fun|launch\s*\{|async\s*\{|withContext\s*\(|supervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{))/.test(sanitizedSource)
  );
};

export const hasKotlinThreadSleepCall = (source: string): boolean => {
  return scanCodeLikeSource(source, ({ source: kotlinSource, index, current }) => {
    if (current !== 'T') {
      return false;
    }

    return (
      hasIdentifierAt(kotlinSource, index, 'Thread') &&
      kotlinSource.startsWith('.sleep', index + 'Thread'.length)
    );
  });
};

export const hasKotlinGlobalScopeUsage = (source: string): boolean => {
  return scanCodeLikeSource(source, ({ source: kotlinSource, index, current }) => {
    if (current !== 'G' || !hasIdentifierAt(kotlinSource, index, 'GlobalScope')) {
      return false;
    }

    const start = index + 'GlobalScope'.length;
    const tail = kotlinSource.slice(start, start + 32);
    return /^\s*\.(launch|async|produce|actor)\b/.test(tail);
  });
};

export const hasKotlinRunBlockingUsage = (source: string): boolean => {
  return scanCodeLikeSource(source, ({ source: kotlinSource, index, current }) => {
    if (current !== 'r' || !hasIdentifierAt(kotlinSource, index, 'runBlocking')) {
      return false;
    }

    const start = index + 'runBlocking'.length;
    const tail = kotlinSource.slice(start, start + 48);
    return /^\s*(<[^>\n]+>\s*)?(\(|\{)/.test(tail);
  });
};

export const hasAndroidAsyncTaskUsage = (source: string): boolean => {
  return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
    if (current !== 'A' && current !== 'a') {
      return false;
    }

    const isBareAsyncTask = hasIdentifierAt(androidSource, index, 'AsyncTask');
    const isQualifiedAsyncTask =
      hasIdentifierAt(androidSource, index, 'android') &&
      androidSource.startsWith('.os.AsyncTask', index + 'android'.length);

    if (!isBareAsyncTask && !isQualifiedAsyncTask) {
      return false;
    }

    const start = isBareAsyncTask
      ? index + 'AsyncTask'.length
      : index + 'android.os.AsyncTask'.length;
    const tail = androidSource.slice(start, start + 48);
    return /^\s*(<[^>\n]+>\s*)?(?:\(|\{|:|\.|$)/.test(tail);
  });
};

const androidActivityBaseTypes = new Set([
  'Activity',
  'ComponentActivity',
  'AppCompatActivity',
  'FragmentActivity',
]);

export const hasKotlinGodActivityUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  const declarations = parseKotlinTypeDeclarations(source);

  return declarations.some((declaration) => {
    const isActivity = declaration.conformances.some((conformance) =>
      androidActivityBaseTypes.has(conformance)
    );
    if (!isActivity) {
      return false;
    }

    const body = lines
      .slice(declaration.bodyStartLine - 1, declaration.bodyEndLine)
      .map((line) => stripKotlinLineForSemanticScan(line))
      .filter((line) => !line.trimStart().startsWith('import '))
      .join('\n');

    const hasUiEntrypoint = /\bsetContent\s*\{/.test(body) || /@Composable\b/.test(body);
    const hasNetworkResponsibility =
      /\b(?:OkHttpClient|Retrofit|HttpURLConnection|newCall|enqueue|execute)\b/.test(body);
    const hasPersistenceResponsibility =
      /\b(?:Room\.databaseBuilder|SharedPreferences|getSharedPreferences|DataStore|SQLiteDatabase)\b/.test(body);
    const hasNavigationResponsibility =
      /\b(?:NavController|rememberNavController|navigate\s*\(|NavHost\s*\()\b/.test(body);
    const functionCount = (body.match(/\bfun\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/g) ?? []).length;
    const activityBodyLines = Math.max(0, declaration.bodyEndLine - declaration.bodyStartLine + 1);

    return (
      (hasUiEntrypoint &&
        [hasNetworkResponsibility, hasPersistenceResponsibility, hasNavigationResponsibility].filter(Boolean)
          .length >= 2) ||
      (hasUiEntrypoint && functionCount >= 8) ||
      activityBodyLines >= 140
    );
  });
};

export const hasKotlinApplicationOnCreateHeavyInitializationUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  const declarations = parseKotlinTypeDeclarations(source);

  return declarations.some((declaration) => {
    const isApplication = declaration.conformances.some((conformance) =>
      conformance === 'Application'
    );
    if (!isApplication) {
      return false;
    }

    const body = lines
      .slice(declaration.bodyStartLine - 1, declaration.bodyEndLine)
      .map((line) => stripKotlinLineForSemanticScan(line))
      .filter((line) => !line.trimStart().startsWith('import '))
      .join('\n');

    const onCreateMatch = /\boverride\s+fun\s+onCreate\s*\([^)]*\)\s*\{/g.exec(body);
    if (!onCreateMatch) {
      return false;
    }

    const openBraceIndex = body.indexOf('{', onCreateMatch.index);
    const onCreateBody = extractBalancedBlockSegment(body, openBraceIndex);
    if (!onCreateBody) {
      return false;
    }

    return /\b(?:FirebaseApp|WorkManager|Room|Retrofit|OkHttpClient|Glide|Coil|Timber)\s*(?:\.|Builder\s*\(|\()/.test(onCreateBody);
  });
};

export const hasKotlinNonLazyScrollableCollectionUsage = (source: string): boolean => {
  const sanitizedSource = source
    .split(/\r?\n/)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .filter((line) => !line.trimStart().startsWith('import '))
    .join('\n');

  const scrollableColumnOrRowPattern =
    /\b(?:Column|Row)\s*\([^)]*\bModifier\s*\.[^)]*(?:verticalScroll|horizontalScroll)\s*\([^)]*\)[\s\S]*?\{([\s\S]*?)\n\s*\}/g;

  let match: RegExpExecArray | null;
  while ((match = scrollableColumnOrRowPattern.exec(sanitizedSource)) !== null) {
    const body = match[1] ?? '';
    if (/\b(?:forEach|forEachIndexed|map|repeat)\s*(?:\(|\{)|\bfor\s*\([^)]+\bin\b[^)]*\)/.test(body)) {
      return true;
    }
  }

  return false;
};

export const hasKotlinUnstableLaunchedEffectKeyUsage = (source: string): boolean => {
  return collectKotlinRegexLines(
    source,
    /\bLaunchedEffect\s*\(\s*(?:Unit|true|false|null)?\s*\)\s*\{/
  ).length > 0;
};

export const hasKotlinLaunchedEffectBusyLoopUsage = (source: string): boolean => {
  const sanitizedSource = source
    .split(/\r?\n/)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .filter((line) => !line.trimStart().startsWith('import '))
    .join('\n');

  const launchedEffectPattern = /\bLaunchedEffect\s*\([^)]*\)\s*\{/g;
  let match: RegExpExecArray | null;
  while ((match = launchedEffectPattern.exec(sanitizedSource)) !== null) {
    const bodyStartIndex = sanitizedSource.indexOf('{', match.index);
    const segment = extractBalancedBlockSegment(sanitizedSource, bodyStartIndex);
    if (
      segment &&
      /\bwhile\s*\(\s*(?:true|isActive)\s*\)\s*\{/.test(segment) &&
      !/\bdelay\s*\(/.test(segment)
    ) {
      return true;
    }
  }

  return false;
};

export const hasKotlinProductionLoggingUsage = (source: string): boolean => {
  return source.split(/\r?\n/).some((line) => {
    const sanitized = stripKotlinLineForSemanticScan(line);
    if (sanitized.trimStart().startsWith('import ') || sanitized.includes('BuildConfig.DEBUG')) {
      return false;
    }
    return /\b(?:println\s*\(|System\s*\.\s*(?:out|err)\s*\.\s*println\s*\(|Log\s*\.\s*(?:v|d|i|w|e|wtf)\s*\(|Timber\s*\.\s*(?:v|d|i|w|e|wtf)\s*\()/.test(sanitized);
  });
};

export const hasKotlinModifierBackgroundBeforePaddingUsage = (source: string): boolean => {
  const sanitizedSource = source
    .split(/\r?\n/)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .filter((line) => !line.trimStart().startsWith('import '))
    .join('\n');

  return /\bModifier\b[\s\S]*?\.background\s*\([^)]*\)[\s\S]*?\.padding\s*\(/.test(sanitizedSource);
};

export const hasKotlinMissingContentDescriptionUsage = (source: string): boolean => {
  const sanitizedSource = source
    .split(/\r?\n/)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .filter((line) => !line.trimStart().startsWith('import '))
    .join('\n');

  const callPattern = /\b(?:Image|Icon)\s*\(/g;
  let match: RegExpExecArray | null;
  while ((match = callPattern.exec(sanitizedSource)) !== null) {
    const openParenIndex = sanitizedSource.indexOf('(', match.index);
    const segment = extractBalancedCallSegment(sanitizedSource, openParenIndex);
    if (segment && !/\bcontentDescription\s*=/.test(segment)) {
      return true;
    }
  }

  return false;
};

export const hasKotlinFontScaleDisabledUsage = (source: string): boolean => {
  return collectKotlinRegexLines(
    source,
    /\bfontScale\s*=\s*(?:1(?:\.0)?f?|1(?:\.0)?)\b/
  ).length > 0;
};

export const hasKotlinIncompleteMaterialThemeUsage = (source: string): boolean => {
  const sanitizedSource = source
    .split(/\r?\n/)
    .map((line) => stripKotlinLineForSemanticScan(line))
    .filter((line) => !line.trimStart().startsWith('import '))
    .join('\n');

  const callPattern = /\bMaterialTheme\s*\(/g;
  let match: RegExpExecArray | null;
  while ((match = callPattern.exec(sanitizedSource)) !== null) {
    const openParenIndex = sanitizedSource.indexOf('(', match.index);
    const segment = extractBalancedCallSegment(sanitizedSource, openParenIndex);
    if (
      segment &&
      (!/\bcolorScheme\s*=/.test(segment) ||
        !/\btypography\s*=/.test(segment) ||
        !/\bshapes\s*=/.test(segment))
    ) {
      return true;
    }
  }

  return false;
};

export const hasKotlinLegacyBottomNavigationUsage = (source: string): boolean => {
  return collectKotlinRegexLines(
    source,
    /\b(?:BottomNavigation|BottomNavigationItem)\s*(?:\(|\{)/
  ).length > 0;
};

export const hasKotlinImperativeNavigationUsage = (source: string): boolean => {
  return collectKotlinRegexLines(
    source,
    /\b(?:startActivity\s*\(\s*Intent\s*\(|supportFragmentManager\s*\.\s*beginTransaction\s*\(|childFragmentManager\s*\.\s*beginTransaction\s*\(|parentFragmentManager\s*\.\s*beginTransaction\s*\(|FragmentTransaction\b|FragmentManager\b)/
  ).length > 0;
};

export const hasKotlinComposableObjectCreationWithoutRememberUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  let pendingComposableAnnotation = false;
  let insideComposable = false;
  let braceDepth = 0;

  for (const rawLine of lines) {
    const sanitized = stripKotlinLineForSemanticScan(rawLine);
    if (sanitized.trimStart().startsWith('import ')) {
      continue;
    }

    if (/@Composable\b/.test(sanitized)) {
      pendingComposableAnnotation = true;
    }

    if (pendingComposableAnnotation && /\bfun\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(sanitized)) {
      insideComposable = true;
      pendingComposableAnnotation = false;
      braceDepth = 0;
    }

    if (insideComposable) {
      const createsObject =
        /\b(?:Regex|SimpleDateFormat|DecimalFormat)\s*\(/.test(sanitized) ||
        /\bDateTimeFormatter\s*\.\s*ofPattern\s*\(/.test(sanitized);
      if (createsObject && !/\bremember\s*\{/.test(sanitized)) {
        return true;
      }

      braceDepth += countTokenOccurrences(sanitized, '{');
      braceDepth -= countTokenOccurrences(sanitized, '}');
      if (braceDepth <= 0 && sanitized.includes('}')) {
        insideComposable = false;
      }
    }
  }

  return false;
};

export const hasKotlinComposableStateCreationWithoutRememberUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  let pendingComposableAnnotation = false;
  let insideComposable = false;
  let braceDepth = 0;

  for (const rawLine of lines) {
    const sanitized = stripKotlinLineForSemanticScan(rawLine);
    if (sanitized.trimStart().startsWith('import ')) {
      continue;
    }

    if (/@Composable\b/.test(sanitized)) {
      pendingComposableAnnotation = true;
    }

    if (pendingComposableAnnotation && /\bfun\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(sanitized)) {
      insideComposable = true;
      pendingComposableAnnotation = false;
      braceDepth = 0;
    }

    if (insideComposable) {
      const createsState = /\b(?:mutableStateOf|derivedStateOf)\s*\(/.test(sanitized);
      if (createsState && !/\bremember\s*\{/.test(sanitized)) {
        return true;
      }

      braceDepth += countTokenOccurrences(sanitized, '{');
      braceDepth -= countTokenOccurrences(sanitized, '}');
      if (braceDepth <= 0 && sanitized.includes('}')) {
        insideComposable = false;
      }
    }
  }

  return false;
};

export const hasKotlinForceUnwrapUsage = (source: string): boolean => {
  return source.split(/\r?\n/).some((line) => {
    const sanitized = stripKotlinLineForSemanticScan(line);
    if (sanitized.trimStart().startsWith('import ')) {
      return false;
    }
    return /!!(?!\s*(?:=|is))/.test(sanitized);
  });
};

export const hasKotlinLiveDataStateExposureUsage = (source: string): boolean => {
  return collectKotlinRegexLines(
    source,
    /\b(?:MutableLiveData|LiveData)\s*(?:<|\(|\.)/
  ).length > 0;
};

export const hasKotlinViewModelFlowWithoutStateInUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  let insideViewModel = false;
  let braceDepth = 0;
  let exposesFlowState = false;
  let usesStateIn = false;

  for (const rawLine of lines) {
    const sanitized = stripKotlinLineForSemanticScan(rawLine);
    if (sanitized.trimStart().startsWith('import ')) {
      continue;
    }

    if (!insideViewModel && /\bclass\s+\w*ViewModel\b/.test(sanitized)) {
      insideViewModel = true;
      braceDepth =
        countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
    } else if (insideViewModel) {
      braceDepth += countTokenOccurrences(sanitized, '{');
      braceDepth -= countTokenOccurrences(sanitized, '}');
    }

    if (insideViewModel) {
      if (/\b(?:val|var)\s+\w+\s*:\s*Flow\s*</.test(sanitized)) {
        exposesFlowState = true;
      }
      if (/\.\s*stateIn\s*\(/.test(sanitized)) {
        usesStateIn = true;
      }
    }

    if (insideViewModel && braceDepth <= 0 && sanitized.includes('}')) {
      if (exposesFlowState && !usesStateIn) {
        return true;
      }
      insideViewModel = false;
      exposesFlowState = false;
      usesStateIn = false;
    }
  }

  return insideViewModel && exposesFlowState && !usesStateIn;
};

export const hasKotlinSharedFlowUsedAsStateUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  let insideViewModel = false;
  let braceDepth = 0;

  for (const rawLine of lines) {
    const sanitized = stripKotlinLineForSemanticScan(rawLine);
    if (sanitized.trimStart().startsWith('import ')) {
      continue;
    }

    if (!insideViewModel && /\bclass\s+\w*ViewModel\b/.test(sanitized)) {
      insideViewModel = true;
      braceDepth =
        countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
    } else if (insideViewModel) {
      braceDepth += countTokenOccurrences(sanitized, '{');
      braceDepth -= countTokenOccurrences(sanitized, '}');
    }

    if (
      insideViewModel &&
      /\b(?:val|var)\s+(?:_)?(?:uiState|state|screenState|viewState)\b[^:\n]*:\s*(?:Mutable)?SharedFlow\s*</.test(sanitized)
    ) {
      return true;
    }

    if (
      insideViewModel &&
      /\b(?:val|var)\s+\w+\s*:\s*(?:Mutable)?SharedFlow\s*<\s*[A-Za-z_][A-Za-z0-9_]*State\s*>/.test(sanitized)
    ) {
      return true;
    }

    if (insideViewModel && braceDepth <= 0 && sanitized.includes('}')) {
      insideViewModel = false;
    }
  }

  return false;
};

export const hasKotlinManualCoroutineScopeInViewModelUsage = (source: string): boolean => {
  const lines = source.split(/\r?\n/);
  let insideViewModel = false;
  let braceDepth = 0;

  for (const rawLine of lines) {
    const sanitized = stripKotlinLineForSemanticScan(rawLine);
    if (sanitized.trimStart().startsWith('import ')) {
      continue;
    }

    if (!insideViewModel && /\bclass\s+\w*ViewModel\b/.test(sanitized)) {
      insideViewModel = true;
      braceDepth =
        countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
    } else if (insideViewModel) {
      braceDepth += countTokenOccurrences(sanitized, '{');
      braceDepth -= countTokenOccurrences(sanitized, '}');
    }

    if (insideViewModel && /\bCoroutineScope\s*\(/.test(sanitized)) {
      return true;
    }

    if (insideViewModel && braceDepth <= 0 && sanitized.includes('}')) {
      insideViewModel = false;
    }
  }

  return false;
};

export const hasKotlinDispatcherMainBoundaryLeakUsage = (source: string): boolean => {
  return collectKotlinRegexLines(source, /\bDispatchers\s*\.\s*Main\b/).length > 0;
};

export const findKotlinPresentationSrpMatch = (
  source: string
): KotlinPresentationSrpMatch | undefined => {
  const classPattern =
    /\b(?:internal\s+|private\s+|public\s+)?(?:sealed\s+|data\s+)?class\s+([A-Za-z0-9_]*(?:ViewModel|Presenter))\b/;
  const classLines = collectKotlinRegexLines(source, classPattern);
  if (classLines.length === 0) {
    return undefined;
  }

  const classLine = source.split(/\r?\n/)[classLines[0] - 1] ?? '';
  const className = classLine.match(classPattern)?.[1];
  if (!className) {
    return undefined;
  }

  const responsibilities: KotlinResponsibilityMatch[] = [];
  const registerNode = (
    key: string,
    kind: KotlinSemanticNodeMatch['kind'],
    name: string,
    regex: RegExp
  ): void => {
    registerKotlinResponsibility(responsibilities, key, kind, name, collectKotlinRegexLines(source, regex));
  };

  registerNode(
    'session',
    'member',
    'session/auth flow',
    /\b(?:restore|bootstrap|refresh|resume|signIn|signOut|authenticate|session)\w*\s*\(/
  );
  registerNode(
    'networking',
    'call',
    'remote networking',
    /\b(?:OkHttpClient\s*\(|Retrofit\.Builder\s*\(|HttpURLConnection\b)/
  );
  registerNode(
    'persistence',
    'call',
    'local persistence',
    /\b(?:SharedPreferences\b.*\)|PreferenceDataStoreFactory\.create|preferencesDataStore|DataStore<|RoomDatabase\b)/
  );
  registerNode(
    'navigation',
    'member',
    'navigation flow',
    /\b(?:findNavController\s*\(|\.\s*navigate\s*\()/
  );

  if (!hasKotlinResponsibilityKeys(responsibilities, ['session', 'networking', 'persistence', 'navigation'])) {
    return undefined;
  }

  const relatedNodes = responsibilities.map((entry) => entry.node);
  const allLines = sortedUniqueLines([
    ...classLines,
    ...relatedNodes.flatMap((node) => [...node.lines]),
  ]);

  return {
    primary_node: {
      kind: 'class',
      name: className,
      lines: classLines,
    },
    related_nodes: relatedNodes,
    why: `${className} concentra session/auth flow, networking remoto, persistencia local y navegación dentro del mismo tipo de presentation Android, rompiendo SRP.`,
    impact:
      'Presentation acumula múltiples razones de cambio y queda más frágil ante cambios de sesión, transporte, almacenamiento o navegación.',
    expected_fix:
      'Deja el tipo limitado a estado observable y delegación; extrae sesión, persistencia, networking y navegación a casos de uso o coordinadores dedicados.',
    lines: allLines,
  };
};

export const findKotlinConcreteDependencyDipMatch = (
  source: string
): KotlinConcreteDependencyDipMatch | undefined => {
  const classPattern =
    /\b(?:internal\s+|private\s+|public\s+)?(?:sealed\s+|data\s+)?class\s+([A-Za-z0-9_]*(?:UseCase|Service|ViewModel|Presenter|Controller|Coordinator))\b/;
  const classLines = collectKotlinRegexLines(source, classPattern);
  if (classLines.length === 0) {
    return undefined;
  }

  const classLine = source.split(/\r?\n/)[classLines[0] - 1] ?? '';
  const className = classLine.match(classPattern)?.[1];
  if (!className) {
    return undefined;
  }

  const relatedNodes: KotlinSemanticNodeMatch[] = [];
  const registerNode = (
    kind: KotlinSemanticNodeMatch['kind'],
    name: string,
    regex: RegExp
  ): void => {
    const lines = collectKotlinRegexLines(source, regex);
    if (lines.length === 0) {
      return;
    }
    relatedNodes.push({ kind, name, lines });
  };

  registerNode(
    'property',
    'concrete dependency: SharedPreferences',
    /\b(?:private|internal|public)?\s*val\s+\w+\s*:\s*SharedPreferences\b/
  );
  registerNode(
    'property',
    'concrete dependency: OkHttpClient',
    /\b(?:private|internal|public)?\s*val\s+\w+\s*:\s*OkHttpClient\b/
  );
  registerNode(
    'property',
    'concrete dependency: Retrofit',
    /\b(?:private|internal|public)?\s*val\s+\w+\s*:\s*Retrofit\b/
  );
  registerNode(
    'property',
    'concrete dependency: DataStore',
    /\b(?:private|internal|public)?\s*val\s+\w+\s*:\s*DataStore(?:<|$)/
  );
  registerNode(
    'property',
    'concrete dependency: RoomDatabase',
    /\b(?:private|internal|public)?\s*val\s+\w+\s*:\s*\w*RoomDatabase\b/
  );
  registerNode('call', 'OkHttpClient()', /\bOkHttpClient\s*\(/);
  registerNode('call', 'Retrofit.Builder()', /\bRetrofit\.Builder\s*\(/);
  registerNode(
    'call',
    'PreferenceDataStoreFactory.create',
    /\bPreferenceDataStoreFactory\.create(?:WithPath)?\s*\(/
  );
  registerNode('call', 'Room.databaseBuilder', /\bRoom\.databaseBuilder\s*\(/);

  if (relatedNodes.length === 0) {
    return undefined;
  }

  const allLines = sortedUniqueLines([
    ...classLines,
    ...relatedNodes.flatMap((node) => [...node.lines]),
  ]);

  return {
    primary_node: {
      kind: 'class',
      name: className,
      lines: classLines,
    },
    related_nodes: relatedNodes,
    why: `${className} depende directamente de servicios concretos del framework o infraestructura en application/presentation Android, rompiendo DIP al saltarse puertos o abstracciones.`,
    impact:
      'La capa de alto nivel queda acoplada a detalles concretos de transporte o persistencia, dificulta el test aislado y aumenta el coste de sustituir infraestructura.',
    expected_fix:
      'Introduce puertos o gateways en application/domain e inyecta adaptadores concretos desde infrastructure, evitando depender directamente de clientes o stores del framework.',
    lines: allLines,
  };
};

export const findKotlinOpenClosedWhenMatch = (
  source: string
): KotlinOpenClosedOcpMatch | undefined => {
  const classPattern =
    /^\s*(?:internal\s+|private\s+|public\s+)?(?:sealed\s+|data\s+)?class\s+([A-Za-z0-9_]*(?:UseCase|Service|ViewModel|Presenter|Controller|Coordinator))\b/;
  const lines = source.split(/\r?\n/);
  const classLines = collectKotlinRegexLines(source, classPattern);
  if (classLines.length === 0) {
    return undefined;
  }

  const classLine = lines[classLines[0] - 1] ?? '';
  const className = classLine.match(classPattern)?.[1];
  if (!className) {
    return undefined;
  }

  for (let index = 0; index < lines.length; index += 1) {
    const sanitizedLine = stripKotlinLineForSemanticScan(lines[index] ?? '');
    const whenMatch = sanitizedLine.match(
      /\bwhen\s*\(\s*(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*\{/
    );
    if (!whenMatch) {
      continue;
    }

    const discriminatorName = whenMatch[1];
    let braceDepth =
      countTokenOccurrences(sanitizedLine, '{') - countTokenOccurrences(sanitizedLine, '}');
    const branchNodes: KotlinSemanticNodeMatch[] = [];

    for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
      const candidateLine = stripKotlinLineForSemanticScan(lines[cursor] ?? '');
      const branchMatch = candidateLine.match(/^\s*(?!else\b)([A-Za-z0-9_.,\s]+?)\s*->/);
      if (branchMatch) {
        const branchName = normalizeKotlinWhenBranchName(branchMatch[1] ?? '');
        if (branchName.length > 0) {
          branchNodes.push({
            kind: 'member',
            name: `branch ${branchName}`,
            lines: [cursor + 1],
          });
        }
      }

      braceDepth += countTokenOccurrences(candidateLine, '{');
      braceDepth -= countTokenOccurrences(candidateLine, '}');
      if (braceDepth <= 0) {
        break;
      }
    }

    const [firstBranchNode, secondBranchNode] = branchNodes;
    if (!firstBranchNode || !secondBranchNode) {
      continue;
    }

    const relatedNodes = [
      {
        kind: 'member' as const,
        name: `discriminator switch: ${discriminatorName}`,
        lines: [index + 1],
      },
      ...branchNodes,
    ];

    const allLines = sortedUniqueLines([
      ...classLines,
      index + 1,
      ...branchNodes.flatMap((node) => [...node.lines]),
    ]);
    const branchSummary = branchNodes
      .map((node) => node.name.replace(/^branch /, ''))
      .join(', ');

    return {
      primary_node: {
        kind: 'class',
        name: className,
        lines: classLines,
      },
      related_nodes: relatedNodes,
      why:
        `${className} resuelve comportamiento con un when sobre ${discriminatorName} ` +
        `(${branchSummary}), obligando a modificar el mismo tipo para soportar un nuevo caso y rompiendo OCP.`,
      impact:
        'Cada nuevo caso exige editar la lógica existente, aumenta el riesgo de regresiones y dificulta extender el comportamiento mediante composición o polimorfismo.',
      expected_fix:
        'Extrae una estrategia, interfaz o registry de handlers por caso para añadir nuevos comportamientos sin modificar la lógica existente.',
      lines: allLines,
    };
  }

  return undefined;
};

export const findKotlinInterfaceSegregationMatch = (
  source: string
): KotlinInterfaceSegregationMatch | undefined => {
  const typePattern =
    /^\s*(?:internal\s+|private\s+|public\s+)?(?:sealed\s+|data\s+)?class\s+([A-Za-z0-9_]*(?:UseCase|ViewModel|Presenter|Controller|Coordinator|Service))\b/;
  const typeLines = collectKotlinRegexLines(source, typePattern);
  if (typeLines.length === 0) {
    return undefined;
  }

  const typeLine = source.split(/\r?\n/)[typeLines[0] - 1] ?? '';
  const typeName = typeLine.match(typePattern)?.[1];
  if (!typeName) {
    return undefined;
  }

  const interfaceDeclarations = parseKotlinInterfaceDeclarations(source);
  if (interfaceDeclarations.length === 0) {
    return undefined;
  }

  const sourceLines = source.split(/\r?\n/);

  for (const interfaceDeclaration of interfaceDeclarations) {
    const queryMembers = interfaceDeclaration.members.filter((member) =>
      isKotlinQueryMemberName(member.name)
    );
    const commandMembers = interfaceDeclaration.members.filter((member) =>
      isKotlinCommandMemberName(member.name)
    );
    if (queryMembers.length === 0 || commandMembers.length === 0) {
      continue;
    }

    const propertyPattern = new RegExp(
      `\\b(?:private|internal|public)?\\s*val\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*:\\s*${interfaceDeclaration.name}\\b`
    );
    const propertyLines = collectKotlinRegexLines(source, propertyPattern);
    if (propertyLines.length === 0) {
      continue;
    }

    const propertyLine = sourceLines[propertyLines[0] - 1] ?? '';
    const propertyName = propertyLine.match(propertyPattern)?.[1];
    if (!propertyName) {
      continue;
    }

    const usedMembers = new Map<string, number[]>();
    const memberUsagePattern = new RegExp(
      `\\b${propertyName}\\.([A-Za-z_][A-Za-z0-9_]*)\\s*(?:\\(|\\b)`,
      'g'
    );

    sourceLines.forEach((line, index) => {
      const sanitizedLine = stripKotlinLineForSemanticScan(line);
      for (const match of sanitizedLine.matchAll(memberUsagePattern)) {
        const memberName = match[1];
        if (!memberName) {
          continue;
        }

        const existingLines = usedMembers.get(memberName) ?? [];
        existingLines.push(index + 1);
        usedMembers.set(memberName, existingLines);
      }
    });

    const usedMemberNames = [...usedMembers.keys()];
    if (usedMemberNames.length === 0) {
      continue;
    }

    const usesQueryContract = usedMemberNames.some(isKotlinQueryMemberName);
    const usesCommandContract = usedMemberNames.some(isKotlinCommandMemberName);
    if (usesQueryContract === usesCommandContract) {
      continue;
    }

    const oppositeFamilyMembers = usesQueryContract ? commandMembers : queryMembers;
    const unusedMembers = oppositeFamilyMembers.filter((member) => !usedMembers.has(member.name));
    const firstUnusedMember = unusedMembers[0];
    if (!firstUnusedMember) {
      continue;
    }

    const usedDescriptors = Array.from(usedMembers.entries()).map(([name, lines]) => ({
      kind: 'call' as const,
      name: `used member: ${name}`,
      lines: sortedUniqueLines(lines),
    }));

    const relatedNodes: KotlinSemanticNodeMatch[] = [
      {
        kind: 'member',
        name: `fat interface: ${interfaceDeclaration.name}`,
        lines: [interfaceDeclaration.line],
      },
      ...usedDescriptors,
      ...unusedMembers.map((member) => ({
        kind: 'member' as const,
        name: `unused contract member: ${member.name}`,
        lines: [member.line],
      })),
    ];

    const usedMemberSummary = usedDescriptors.map((member) => member.name.replace('used member: ', ''));
    const unusedSummary = unusedMembers.map((member) => member.name);
    const allLines = sortedUniqueLines([
      ...typeLines,
      interfaceDeclaration.line,
      ...relatedNodes.flatMap((node) => [...node.lines]),
    ]);

    return {
      primary_node: {
        kind: 'class',
        name: typeName,
        lines: typeLines,
      },
      related_nodes: relatedNodes,
      why:
        `${typeName} depende de ${interfaceDeclaration.name}, un contrato demasiado ancho para su ` +
        `uso real del consumidor, y rompe ISP al acoplarlo a miembros que no necesita.`,
      impact:
        `El consumidor queda acoplado a cambios ajenos del contrato (${unusedSummary.join(', ')}), ` +
        'lo que eleva el coste de mantenimiento y dificulta tests y evolución independiente.',
      expected_fix:
        `Extrae interfaces pequeñas orientadas a capacidades concretas (${usedMemberSummary.join(', ')}) ` +
        'o inyecta un puerto mínimo que exponga solo los miembros realmente necesarios.',
      lines: allLines,
    };
  }

  return undefined;
};

export const findKotlinLiskovSubstitutionMatch = (
  source: string
): KotlinLiskovSubstitutionMatch | undefined => {
  const interfaceDeclarations = parseKotlinInterfaceDeclarations(source);
  if (interfaceDeclarations.length === 0) {
    return undefined;
  }

  const typeDeclarations = parseKotlinTypeDeclarations(source);

  const sourceLines = source.split(/\r?\n/);

  for (const interfaceDeclaration of interfaceDeclarations) {
    const memberNames = interfaceDeclaration.members.map((member) => member.name);
    if (memberNames.length === 0) {
      continue;
    }

    const conformingTypes = typeDeclarations.filter((typeDeclaration) =>
      typeDeclaration.conformances.includes(interfaceDeclaration.name)
    );

    for (const memberName of memberNames) {
      let safeType: KotlinTypeDeclaration | undefined;
      let unsafeType:
        | (KotlinTypeDeclaration & {
            narrowedPreconditionLine: number;
            failureLine: number;
            failureName: string;
          })
        | undefined;

      for (const typeDeclaration of conformingTypes) {
        const methodPattern = new RegExp(`\\boverride\\s+fun\\s+${memberName}\\s*\\(`);
        let methodLine = -1;

        for (
          let lineIndex = typeDeclaration.bodyStartLine - 1;
          lineIndex < typeDeclaration.bodyEndLine;
          lineIndex += 1
        ) {
          const candidateLine = stripKotlinLineForSemanticScan(sourceLines[lineIndex] ?? '');
          if (methodPattern.test(candidateLine)) {
            methodLine = lineIndex + 1;
            break;
          }
        }

        if (methodLine < 0) {
          continue;
        }

        let methodBraceDepth =
          countTokenOccurrences(stripKotlinLineForSemanticScan(sourceLines[methodLine - 1] ?? ''), '{')
          - countTokenOccurrences(stripKotlinLineForSemanticScan(sourceLines[methodLine - 1] ?? ''), '}');
        let narrowedPreconditionLine: number | undefined;
        let failureLine: number | undefined;
        let failureName: string | undefined;

        for (
          let lineIndex = methodLine;
          lineIndex < typeDeclaration.bodyEndLine && methodBraceDepth > 0;
          lineIndex += 1
        ) {
          const candidateLine = stripKotlinLineForSemanticScan(sourceLines[lineIndex] ?? '');
          if (
            narrowedPreconditionLine === undefined &&
            /\b(?:require|check)\s*\(/.test(candidateLine)
          ) {
            narrowedPreconditionLine = lineIndex + 1;
          }
          if (failureLine === undefined) {
            const failureMatch = candidateLine.match(
              /\b(error|TODO)\s*\(|\bthrow\s+(UnsupportedOperationException|NotImplementedError|IllegalStateException)\b/
            );
            if (failureMatch) {
              failureLine = lineIndex + 1;
              failureName = failureMatch[1] ?? failureMatch[2] ?? 'throw';
            }
          }
          methodBraceDepth += countTokenOccurrences(candidateLine, '{');
          methodBraceDepth -= countTokenOccurrences(candidateLine, '}');
        }

        if (
          narrowedPreconditionLine !== undefined &&
          failureLine !== undefined &&
          failureName !== undefined
        ) {
          unsafeType = {
            ...typeDeclaration,
            narrowedPreconditionLine,
            failureLine,
            failureName,
          };
        } else if (!safeType) {
          safeType = typeDeclaration;
        }
      }

      if (!safeType || !unsafeType) {
        continue;
      }

      const allLines = sortedUniqueLines([
        interfaceDeclaration.line,
        safeType.line,
        unsafeType.line,
        unsafeType.narrowedPreconditionLine,
        unsafeType.failureLine,
      ]);

      return {
        primary_node: {
          kind: 'class',
          name: unsafeType.name,
          lines: [unsafeType.line],
        },
        related_nodes: [
          {
            kind: 'member',
            name: `base contract: ${interfaceDeclaration.name}`,
            lines: [interfaceDeclaration.line],
          },
          {
            kind: 'member',
            name: `safe substitute: ${safeType.name}`,
            lines: [safeType.line],
          },
          {
            kind: 'member',
            name: `narrowed precondition: ${memberName}`,
            lines: [unsafeType.narrowedPreconditionLine],
          },
          {
            kind: 'call',
            name: unsafeType.failureName,
            lines: [unsafeType.failureLine],
          },
        ],
        why:
          `${unsafeType.name} endurece la precondición de ${memberName} y añade una ruta no segura ` +
          `frente al contrato ${interfaceDeclaration.name}, rompiendo LSP porque deja de ser sustituible por el subtipo base.`,
        impact:
          'Los consumidores del contrato base pueden romperse en runtime, introducir ramas especiales o sufrir regresiones cuando reciben el subtipo inseguro.',
        expected_fix:
          'Haz que el subtipo respete el contrato base sin endurecer precondiciones ni rutas no soportadas, o separa el comportamiento incompatible en otra estrategia o abstracción.',
        lines: allLines,
      };
    }
  }

  return undefined;
};

const legacyFingerprintApiPattern =
  /\b(?:android\.hardware\.fingerprint\.)?FingerprintManager(?:Compat)?\b/;

export const hasAndroidLegacyFingerprintApiUsage = (source: string): boolean =>
  scanCodeLikeSource(source, ({ source: scannedSource, index }) => {
    const match = scannedSource.slice(index).match(legacyFingerprintApiPattern);
    return match?.index === 0;
  });

export const collectAndroidLegacyFingerprintApiLines = (
  source: string
): readonly number[] => {
  const lines: number[] = [];
  source.split(/\r?\n/).forEach((line, index) => {
    const sanitized = stripKotlinLineForSemanticScan(line);
    if (legacyFingerprintApiPattern.test(sanitized)) {
      lines.push(index + 1);
    }
  });
  return sortedUniqueLines(lines);
};
