import fs from 'fs';
import { TextDecoder } from 'util';
import type { LintMessage } from './types.js';

const UTF8_BOM = Buffer.from([0xEF, 0xBB, 0xBF]);

export type EolStyle = 'crlf' | 'lf' | 'cr' | 'mixed' | 'none';
export type EolTarget = 'lf' | 'crlf';
export type AppliedFileFormatFix = 'bom' | 'eol';

export interface EolCounts {
  crlf: number;
  lf: number;
  cr: number;
}

export interface FileFormatInspection {
  hasUtf8Bom: boolean;
  validUtf8: boolean;
  eolStyle: EolStyle;
  eolCounts: EolCounts;
}

export interface CfmlFileFormatResult {
  content: Buffer;
  inspection: FileFormatInspection;
  messages: LintMessage[];
  fixesApplied?: AppliedFileFormatFix[];
}

export interface CfmlFileFormatOptions {
  fix: boolean;
  fixEol?: EolTarget;
}

export function hasUtf8BomBytes(content: Uint8Array): boolean {
  return content.length >= UTF8_BOM.length
    && content[0] === UTF8_BOM[0]
    && content[1] === UTF8_BOM[1]
    && content[2] === UTF8_BOM[2];
}

export function detectEol(content: Uint8Array): { style: EolStyle; counts: EolCounts } {
  const counts: EolCounts = { crlf: 0, lf: 0, cr: 0 };

  for (let index = 0; index < content.length; index += 1) {
    if (content[index] === 0x0D) {
      if (content[index + 1] === 0x0A) {
        counts.crlf += 1;
        index += 1;
      } else {
        counts.cr += 1;
      }
    } else if (content[index] === 0x0A) {
      counts.lf += 1;
    }
  }

  const styles = [counts.crlf > 0, counts.lf > 0, counts.cr > 0].filter(Boolean).length;
  if (styles === 0) return { style: 'none', counts };
  if (styles > 1) return { style: 'mixed', counts };
  if (counts.crlf > 0) return { style: 'crlf', counts };
  return counts.lf > 0
    ? { style: 'lf', counts }
    : { style: 'cr', counts };
}

export function inspectFileFormat(content: Uint8Array): FileFormatInspection {
  const buffer = Buffer.from(content);
  const hasUtf8Bom = hasUtf8BomBytes(buffer);
  const payload = hasUtf8Bom ? buffer.subarray(UTF8_BOM.length) : buffer;
  let validUtf8 = true;

  try {
    new TextDecoder('utf-8', { fatal: true }).decode(payload);
  } catch {
    validUtf8 = false;
  }

  const eol = detectEol(payload);
  return {
    hasUtf8Bom,
    validUtf8,
    eolStyle: eol.style,
    eolCounts: eol.counts,
  };
}

/** Converts only CR/LF bytes, preserving all other payload bytes verbatim. */
export function normalizeEolBytes(content: Uint8Array, target: EolTarget): Buffer {
  const input = Buffer.from(content);
  const { counts } = detectEol(input);
  const extraBytes = target === 'crlf' ? counts.lf + counts.cr : -counts.crlf;
  const output = Buffer.allocUnsafe(input.length + extraBytes);
  let outputOffset = 0;

  for (let inputOffset = 0; inputOffset < input.length; inputOffset += 1) {
    const byte = input[inputOffset];
    if (byte === 0x0D && input[inputOffset + 1] === 0x0A) {
      output[outputOffset++] = target === 'crlf' ? 0x0D : 0x0A;
      if (target === 'crlf') output[outputOffset++] = 0x0A;
      inputOffset += 1;
    } else if (byte === 0x0D || byte === 0x0A) {
      if (target === 'crlf') output[outputOffset++] = 0x0D;
      output[outputOffset++] = 0x0A;
    } else {
      output[outputOffset++] = byte;
    }
  }

  return output.subarray(0, outputOffset);
}

export function getFileFormatDiagnostics(inspection: FileFormatInspection): LintMessage[] {
  const messages: LintMessage[] = [];

  if (!inspection.hasUtf8Bom) {
    messages.push({
      line: 1,
      column: 1,
      severity: 'error',
      message: 'CFML file must be encoded in UTF-8 with BOM.',
      ruleId: 'FILE_ENCODING_ERROR',
    });
  }

  if (!inspection.validUtf8) {
    messages.push({
      line: 1,
      column: 1,
      severity: 'error',
      message: 'File payload is not valid UTF-8; automatic payload repair is not supported.',
      ruleId: 'FILE_UTF8_INVALID',
    });
  }

  if (inspection.eolStyle === 'mixed') {
    messages.push({
      line: 1,
      column: 1,
      severity: 'warning',
      message: `CFML file uses mixed line endings (CRLF: ${inspection.eolCounts.crlf}, LF: ${inspection.eolCounts.lf}, CR: ${inspection.eolCounts.cr}).`,
      ruleId: 'FILE_EOL_MIXED',
    });
  }

  return messages;
}

function prepareCfmlFileFormat(content: Buffer, options: CfmlFileFormatOptions): {
  content: Buffer;
  fixesApplied: AppliedFileFormatFix[];
} {
  if (options.fixEol && !options.fix) {
    throw new Error('fix_eol requires fix:true. No file changes were made.');
  }

  const originalInspection = inspectFileFormat(content);
  const fixesApplied: AppliedFileFormatFix[] = [];
  let hasBom = originalInspection.hasUtf8Bom;
  let payload = hasBom ? content.subarray(UTF8_BOM.length) : content;

  if (options.fix && !hasBom) {
    hasBom = true;
    fixesApplied.push('bom');
  }

  if (options.fix && options.fixEol && originalInspection.validUtf8) {
    const normalizedPayload = normalizeEolBytes(payload, options.fixEol);
    if (!normalizedPayload.equals(payload)) {
      payload = normalizedPayload;
      fixesApplied.push('eol');
    }
  }

  return {
    content: hasBom ? Buffer.concat([UTF8_BOM, payload]) : Buffer.from(payload),
    fixesApplied,
  };
}

export function applyCfmlFileFormat(
  filePath: string,
  options: CfmlFileFormatOptions,
): CfmlFileFormatResult {
  const original = fs.readFileSync(filePath);
  const prepared = prepareCfmlFileFormat(original, options);
  let actual = original;

  if (!prepared.content.equals(original)) {
    try {
      fs.writeFileSync(filePath, prepared.content);
    } catch { /* Verify the on-disk result below; a failed write can still leave the requested bytes in place. */ }

    try {
      actual = fs.readFileSync(filePath);
    } catch {
      actual = original;
    }
  }

  const inspection = inspectFileFormat(actual);
  const verified = actual.equals(prepared.content)
    && (!prepared.fixesApplied.includes('bom') || inspection.hasUtf8Bom)
    && (!prepared.fixesApplied.includes('eol')
      || (inspection.validUtf8 && inspection.eolStyle === options.fixEol));
  const fixesApplied = verified && prepared.fixesApplied.length > 0
    ? prepared.fixesApplied
    : undefined;
  const messages = getFileFormatDiagnostics(inspection);

  if (prepared.fixesApplied.length > 0 && !verified) {
    messages.push({
      line: 1,
      column: 1,
      severity: 'error',
      message: 'Requested CFML file-format fixes could not be verified after writing; inspect the file before retrying.',
      ruleId: 'FILE_FORMAT_FIX_FAILED',
    });
  }

  return {
    content: actual,
    inspection,
    messages,
    ...(fixesApplied ? { fixesApplied } : {}),
  };
}
