const path = require('node:path');
const fs = require('fs-extra');
const YAML = require('yaml');
const {
  pathExists,
  ensureDir,
  mergeDirNoConflicts
} = require('../utils/fs');

const PATCH_MANIFEST = 'patch.manifest.json';

function normalizePath(value) {
  return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
}

function normalizeScalar(value) {
  return JSON.stringify(value);
}

function mergeYamlNode(baseNode, patchNode) {
  if (Array.isArray(baseNode) && Array.isArray(patchNode)) {
    const merged = [...baseNode];
    const seen = new Set(baseNode.map((item) => normalizeScalar(item)));
    for (const item of patchNode) {
      const key = normalizeScalar(item);
      if (!seen.has(key)) {
        merged.push(item);
        seen.add(key);
      }
    }
    return merged;
  }

  const baseIsObject =
    baseNode !== null && typeof baseNode === 'object' && !Array.isArray(baseNode);
  const patchIsObject =
    patchNode !== null && typeof patchNode === 'object' && !Array.isArray(patchNode);

  if (baseIsObject && patchIsObject) {
    const merged = { ...baseNode };
    for (const [key, value] of Object.entries(patchNode)) {
      if (Object.prototype.hasOwnProperty.call(merged, key)) {
        merged[key] = mergeYamlNode(merged[key], value);
      } else {
        merged[key] = value;
      }
    }
    return merged;
  }

  return patchNode;
}

function parsePlistEntries(xml) {
  const dictMatch = xml.match(/<dict>([\s\S]*?)<\/dict>/);
  if (!dictMatch) {
    return [];
  }

  const body = dictMatch[1];
  const entries = [];
  const regex = /<key>([^<]+)<\/key>\s*([\s\S]*?)(?=<key>|$)/g;
  let match = regex.exec(body);

  while (match) {
    entries.push({
      key: match[1].trim(),
      valueXml: match[2].trim()
    });
    match = regex.exec(body);
  }

  return entries;
}

function mergeInfoPlist(targetXml, patchXml) {
  const targetEntries = parsePlistEntries(targetXml);
  const patchEntries = parsePlistEntries(patchXml);

  if (targetEntries.length === 0 || patchEntries.length === 0) {
    return targetXml;
  }

  const targetHeaderMatch = targetXml.match(/^([\s\S]*?<dict>)/);
  const targetFooterMatch = targetXml.match(/(<\/dict>[\s\S]*)$/);
  if (!targetHeaderMatch || !targetFooterMatch) {
    return targetXml;
  }

  const orderedKeys = targetEntries.map((entry) => entry.key);
  const mergedByKey = new Map(targetEntries.map((entry) => [entry.key, entry.valueXml]));

  for (const entry of patchEntries) {
    if (!mergedByKey.has(entry.key)) {
      orderedKeys.push(entry.key);
    }
    mergedByKey.set(entry.key, entry.valueXml);
  }

  const mergedBody = orderedKeys
    .map((key) => {
      const valueXml = String(mergedByKey.get(key) || '')
        .split('\n')
        .map((line) => line.trim())
        .join('\n    ');
      return `    <key>${key}</key>\n    ${valueXml}`;
    })
    .join('\n');

  return `${targetHeaderMatch[1]}\n${mergedBody}\n${targetFooterMatch[1]}`;
}

function mapGradleBlocks(lines) {
  const stack = [];
  const blocks = new Map();

  for (let i = 0; i < lines.length; i += 1) {
    const line = lines[i];
    const trimmed = line.trim();

    const openMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*(?:\([^)]*\))?\s*\{$/);
    if (openMatch) {
      const name = openMatch[1];
      stack.push({ name, start: i, depth: stack.length + 1 });
      continue;
    }

    if (trimmed.startsWith('}')) {
      const current = stack.pop();
      if (current) {
        const pathKey = [...stack.map((item) => item.name), current.name].join('.');
        blocks.set(pathKey, {
          start: current.start,
          end: i,
          depth: current.depth
        });
      }
    }
  }

  return blocks;
}

function detectGradlePath(lines, lineIndex) {
  const stack = [];

  for (let i = 0; i <= lineIndex; i += 1) {
    const trimmed = lines[i].trim();
    const openMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*(?:\([^)]*\))?\s*\{$/);
    if (openMatch) {
      stack.push(openMatch[1]);
      continue;
    }

    if (trimmed.startsWith('}')) {
      stack.pop();
    }
  }

  return stack.join('.');
}

function normalizeLine(line) {
  return line.trim().replace(/\s+/g, ' ');
}

function mergeGradleText(targetText, patchText) {
  const targetLines = targetText.split('\n');
  const patchLines = patchText.split('\n');
  const existing = new Set(targetLines.map((line) => normalizeLine(line)).filter(Boolean));

  const groupedByPath = new Map();
  for (let i = 0; i < patchLines.length; i += 1) {
    const line = patchLines[i];
    const trimmed = line.trim();
    if (!trimmed || trimmed === '{' || trimmed === '}' || trimmed.startsWith('//')) {
      continue;
    }

    const normalized = normalizeLine(line);
    if (existing.has(normalized)) {
      continue;
    }

    const pathKey = detectGradlePath(patchLines, i);
    if (!groupedByPath.has(pathKey)) {
      groupedByPath.set(pathKey, []);
    }
    groupedByPath.get(pathKey).push(trimmed);
  }

  if (groupedByPath.size === 0) {
    return targetText;
  }

  let output = [...targetLines];
  for (const [pathKey, linesToInsert] of groupedByPath.entries()) {
    const blocks = mapGradleBlocks(output);
    const block = blocks.get(pathKey);
    const uniqueLines = [];

    for (const line of linesToInsert) {
      if (!existing.has(normalizeLine(line))) {
        uniqueLines.push(line);
        existing.add(normalizeLine(line));
      }
    }

    if (uniqueLines.length === 0) {
      continue;
    }

    if (block) {
      const indent = '  '.repeat(block.depth);
      const insertion = uniqueLines.map((line) => `${indent}${line}`);
      output.splice(block.end, 0, ...insertion);
      continue;
    }

    if (!pathKey) {
      output.push('', ...uniqueLines);
      continue;
    }

    const names = pathKey.split('.');
    const snippet = [];
    let indentLevel = 0;
    for (const name of names) {
      const indent = '  '.repeat(indentLevel);
      snippet.push(`${indent}${name} {`);
      indentLevel += 1;
    }
    for (const line of uniqueLines) {
      snippet.push(`${'  '.repeat(indentLevel)}${line}`);
    }
    while (indentLevel > 0) {
      indentLevel -= 1;
      snippet.push(`${'  '.repeat(indentLevel)}}`);
    }
    output.push('', ...snippet);
  }

  return output.join('\n');
}

async function applyCopyOperation(filePath, patchDir, targetDir) {
  const normalized = normalizePath(filePath);
  const source = path.join(patchDir, 'files', normalized);
  const destination = path.join(targetDir, normalized);
  await ensureDir(path.dirname(destination));

  const sourceExists = await pathExists(source);
  if (!sourceExists) {
    throw new Error(`Patch file not found: ${source}`);
  }

  const destinationExists = await pathExists(destination);
  if (!destinationExists) {
    await fs.copy(source, destination);
    return;
  }

  const [sourceContent, targetContent] = await Promise.all([
    fs.readFile(source),
    fs.readFile(destination)
  ]);

  if (!sourceContent.equals(targetContent)) {
    throw new Error(`Patch conflict on file: ${destination}`);
  }
}

async function applyReplaceOperation(filePath, patchDir, targetDir) {
  const normalized = normalizePath(filePath);
  const source = path.join(patchDir, 'files', normalized);
  const destination = path.join(targetDir, normalized);
  await ensureDir(path.dirname(destination));

  const sourceExists = await pathExists(source);
  if (!sourceExists) {
    throw new Error(`Patch file not found: ${source}`);
  }

  await fs.copy(source, destination);
}

async function applyRemoveOperation(filePath, targetDir) {
  const normalized = normalizePath(filePath);
  const destination = path.join(targetDir, normalized);
  if (await pathExists(destination)) {
    await fs.remove(destination);
  }
}

async function applyMergeOperation(operation, patchDir, targetDir) {
  const normalized = normalizePath(operation.path);
  const sourceRelative = operation.source
    ? normalizePath(operation.source)
    : path.join('merge', normalized);

  const source = path.join(patchDir, sourceRelative);
  const destination = path.join(targetDir, normalized);
  const destinationExists = await pathExists(destination);

  if (!(await pathExists(source))) {
    throw new Error(`Merge source not found: ${source}`);
  }

  if (!destinationExists) {
    await ensureDir(path.dirname(destination));
    await fs.copy(source, destination);
    return;
  }

  const [targetRaw, patchRaw] = await Promise.all([
    fs.readFile(destination, 'utf8'),
    fs.readFile(source, 'utf8')
  ]);

  let mergedRaw = targetRaw;
  switch (operation.strategy) {
    case 'yaml_deep_merge': {
      const targetDoc = YAML.parse(targetRaw) || {};
      const patchDoc = YAML.parse(patchRaw) || {};
      const mergedDoc = mergeYamlNode(targetDoc, patchDoc);
      mergedRaw = YAML.stringify(mergedDoc);
      break;
    }
    case 'plist_key_merge': {
      mergedRaw = mergeInfoPlist(targetRaw, patchRaw);
      break;
    }
    case 'gradle_line_merge': {
      mergedRaw = mergeGradleText(targetRaw, patchRaw);
      break;
    }
    default:
      throw new Error(`Unsupported merge strategy: ${operation.strategy}`);
  }

  if (mergedRaw !== targetRaw) {
    await fs.writeFile(destination, mergedRaw, 'utf8');
  }
}

async function applyManifestPatch(patchDir, targetDir, manifest) {
  const copy = Array.isArray(manifest.copy) ? manifest.copy : [];
  const replace = Array.isArray(manifest.replace) ? manifest.replace : [];
  const remove = Array.isArray(manifest.remove) ? manifest.remove : [];
  const merge = Array.isArray(manifest.merge) ? manifest.merge : [];

  for (const filePath of copy) {
    await applyCopyOperation(filePath, patchDir, targetDir);
  }

  for (const filePath of replace) {
    await applyReplaceOperation(filePath, patchDir, targetDir);
  }

  for (const operation of merge) {
    await applyMergeOperation(operation, patchDir, targetDir);
  }

  for (const filePath of remove) {
    await applyRemoveOperation(filePath, targetDir);
  }
}

async function applyPatchDirectory(patchDir, targetDir) {
  if (!(await pathExists(patchDir))) {
    return;
  }

  const manifestPath = path.join(patchDir, PATCH_MANIFEST);
  if (!(await pathExists(manifestPath))) {
    const isBackend = path.basename(path.dirname(patchDir)) === 'backends';
    const options = {
      exclude: ['pubspec.yaml', 'pubspec.lock', 'lib/app.dart', 'lib/backend_config.dart', 'patch.yaml', 'README.md'],
      overwrite: isBackend ? ['lib/main.dart'] : []
    };
    await mergeDirNoConflicts(patchDir, targetDir, options);
    return;
  }

  const manifest = await fs.readJson(manifestPath);
  await applyManifestPatch(patchDir, targetDir, manifest);
}

module.exports = {
  PATCH_MANIFEST,
  applyPatchDirectory,
  mergeYamlNode,
  mergeInfoPlist,
  mergeGradleText
};
