'use strict';

const path = require('node:path');
const fs = require('fs-extra');
const os = require('node:os');
const kleur = require('kleur');
const ui = require('./ui');
const { exec, spawn } = require('node:child_process');
const { promisify } = require('node:util');

const execAsync = promisify(exec);

const APPLE_ENV_REL = path.join('.kasy', 'apple.env');
const APPLE_ENV_EXAMPLE = path.join('.kasy', 'apple.env.example');
const RELEASE_SCRIPT = path.join('scripts', 'release-ios.sh');

const URL_APP_STORE_CONNECT_APPS = 'https://appstoreconnect.apple.com/apps';
const URL_APP_STORE_CONNECT_API = 'https://appstoreconnect.apple.com/access/integrations/api';

/**
 * @param {string} content
 * @param {string|null} versionNameOverride
 * @returns {{ updated: string, label: string }}
 */
function bumpPubspecVersion(content, versionNameOverride = null) {
  const match = content.match(/^version:\s*(\d+\.\d+\.\d+)\+(\d+)\s*$/m);
  if (!match) {
    throw new Error('Invalid version in pubspec.yaml (expected 1.0.0+1)');
  }
  const name = versionNameOverride || match[1];
  const build = parseInt(match[2], 10) + 1;
  const next = `version: ${name}+${build}`;
  return {
    updated: content.replace(/^version:\s*.*/m, next),
    label: `${name}+${build}`,
  };
}

async function isKasyFlutterProject(projectDir) {
  const pubspec = path.join(projectDir, 'pubspec.yaml');
  const iosDir = path.join(projectDir, 'ios');
  return (await fs.pathExists(pubspec)) && (await fs.pathExists(iosDir));
}

async function readBundleId(projectDir) {
  // Source of truth is the Xcode project file — what the build actually uses
  // when installing on a device. kit_setup.json can drift if the user renames
  // the bundle id manually after `kasy new`, so it's only used as a fallback
  // when the iOS project hasn't been generated yet.
  const pbxPath = path.join(projectDir, 'ios', 'Runner.xcodeproj', 'project.pbxproj');
  if (await fs.pathExists(pbxPath)) {
    const content = await fs.readFile(pbxPath, 'utf8');
    // Match the main target only — skip Widget/NotificationService extensions
    // by picking the first identifier that doesn't contain a dot extension.
    const matches = [...content.matchAll(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g)];
    for (const m of matches) {
      const id = m[1].trim().replace(/"/g, '');
      if (
        !id.endsWith('.HomeWidgetExtension') &&
        !id.endsWith('.NotificationService')
      ) {
        return id;
      }
    }
    if (matches[0]) return matches[0][1].trim().replace(/"/g, '');
  }
  const kitSetupPath = path.join(projectDir, 'kit_setup.json');
  if (await fs.pathExists(kitSetupPath)) {
    try {
      const kit = await fs.readJson(kitSetupPath);
      if (kit.bundleId) return kit.bundleId;
    } catch {
      // ignore
    }
  }
  return null;
}

function applePrivateKeyPath(keyId) {
  return path.join(os.homedir(), '.appstoreconnect', 'private_keys', `AuthKey_${keyId}.p8`);
}

async function loadAppleEnv(projectDir) {
  const envPath = path.join(projectDir, APPLE_ENV_REL);
  if (!(await fs.pathExists(envPath))) {
    return null;
  }
  const content = await fs.readFile(envPath, 'utf8');
  const env = {};
  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const eq = trimmed.indexOf('=');
    if (eq === -1) continue;
    env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
  }
  return env;
}

async function validateAppleSetup(projectDir) {
  const issues = [];
  const env = await loadAppleEnv(projectDir);
  if (!env) {
    issues.push('missing_env');
    return { ok: false, issues, env: null };
  }
  const keyId = env.APP_STORE_API_KEY;
  const issuerId = env.APP_STORE_ISSUER_ID;
  if (!keyId || !issuerId) {
    issues.push('missing_credentials');
  }
  if (keyId) {
    const p8 = applePrivateKeyPath(keyId);
    if (!(await fs.pathExists(p8))) {
      issues.push('missing_p8');
    }
  }
  return {
    ok: issues.length === 0,
    issues,
    env,
  };
}

async function writeAppleEnv(projectDir, { apiKey, issuerId, appId }) {
  const lines = [
    '# Generated by kasy ios configure — do not commit',
    `APP_STORE_API_KEY=${apiKey}`,
    `APP_STORE_ISSUER_ID=${issuerId}`,
  ];
  if (appId) {
    lines.push(`APP_STORE_APP_ID=${appId}`);
  }
  lines.push('');
  await fs.ensureDir(path.join(projectDir, '.kasy'));
  await fs.outputFile(path.join(projectDir, APPLE_ENV_REL), lines.join('\n'), 'utf8');
}

async function installPrivateKey(sourcePath, keyId) {
  const destDir = path.join(os.homedir(), '.appstoreconnect', 'private_keys');
  const destPath = path.join(destDir, `AuthKey_${keyId}.p8`);
  await fs.ensureDir(destDir);
  await fs.copy(sourcePath, destPath, { overwrite: true });
  return destPath;
}

function openUrl(url) {
  const platform = process.platform;
  const cmd =
    platform === 'darwin'
      ? `open "${url}"`
      : platform === 'win32'
        ? `start "" "${url}"`
        : `xdg-open "${url}"`;
  exec(cmd, () => {});
}

function iosReleaseDocPath() {
  return 'docs/ios-release.md';
}

/**
 * Kasy removes DEVELOPMENT_TEAM from generated projects — user must set signing in Xcode once.
 */
async function checkIosSigning(projectDir) {
  const pbxPath = path.join(projectDir, 'ios', 'Runner.xcodeproj', 'project.pbxproj');
  if (!(await fs.pathExists(pbxPath))) {
    return { ok: false, reason: 'no_pbxproj' };
  }
  const content = await fs.readFile(pbxPath, 'utf8');
  if (/DEVELOPMENT_TEAM\s*=\s*[A-Z0-9]{10};/.test(content)) {
    return { ok: true };
  }
  return { ok: false, reason: 'no_team' };
}

const MIN_DISK_GB_FOR_IOS_BUILD = 15;

const XCODE_CACHE_ERROR_PATTERNS = [
  /disk I\/O error/i,
  /DerivedData/i,
  /build\.db/i,
  /unable to rename/i,
  /database is locked/i,
  /clang: error: no such file or directory:.*\.swiftmodule/i,
];

const POD_NETWORK_ERROR_PATTERNS = [
  /curl:\s*\(6\)/i,
  /curl:\s*\(7\)/i,
  /curl:\s*\(28\)/i,
  /curl:\s*\(35\)/i,
  /curl:\s*\(56\)/i,
  /Connection reset by peer/i,
  /Could not resolve host/i,
  /Network is unreachable/i,
  /Operation timed out/i,
];

async function getFreeDiskGb(checkPath) {
  if (process.platform !== 'darwin' && process.platform !== 'linux') {
    return null;
  }
  try {
    const target = checkPath || process.cwd();
    const { stdout } = await execAsync(`df -g "${target}" | tail -1`);
    const parts = stdout.trim().split(/\s+/);
    const avail = parseInt(parts[3], 10);
    return Number.isFinite(avail) ? avail : null;
  } catch {
    return null;
  }
}

/**
 * @returns {{ ok: boolean, freeGb: number|null }}
 */
async function checkDiskSpaceForIosBuild(projectDir) {
  const freeGb = await getFreeDiskGb(projectDir);
  if (freeGb === null) {
    return { ok: true, freeGb: null };
  }
  return { ok: freeGb >= MIN_DISK_GB_FOR_IOS_BUILD, freeGb };
}

function isXcodeCacheBuildError(output) {
  const text = String(output || '');
  return XCODE_CACHE_ERROR_PATTERNS.some((re) => re.test(text));
}

function isPodNetworkError(output) {
  const text = String(output || '');
  return POD_NETWORK_ERROR_PATTERNS.some((re) => re.test(text));
}

function printBuildFailureHints(t, projectDir) {
  const name = path.basename(projectDir);
  const cleanArg = projectDir !== process.cwd() ? ` ${projectDir}` : '';
  const lines = [
    `1. ${t('ios.hints.closeXcode')}`,
    `2. ${kleur.cyan(`kasy ios clean${cleanArg}`)}`,
    `3. ${t('ios.hints.retry')}: ${kleur.cyan('kasy ios release')}`,
  ];
  if (name) {
    lines.push('');
    lines.push(kleur.dim(`${t('ios.hints.manual')}:`));
    lines.push(kleur.dim(`  cd ${projectDir}`));
    lines.push(kleur.dim('  flutter clean && flutter pub get'));
    lines.push(kleur.dim('  rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-*'));
    lines.push(kleur.dim('  cd ios && pod install'));
  }
  ui.note(lines.join('\n'), t('ios.hints.title'));
}

function printPodNetworkHints(t) {
  const lines = [
    t('ios.hints.network.body'),
    '',
    `1. ${t('ios.hints.network.step1')}`,
    `2. ${t('ios.hints.network.step2')}: ${kleur.cyan('kasy ios')}`,
    `3. ${t('ios.hints.network.step3')}: ${kleur.cyan('kasy ios clean')}`,
  ];
  ui.note(lines.join('\n'), t('ios.hints.network.title'));
}

async function runIosClean(projectDir, t) {
  const steps = [
    { label: t('ios.clean.step.flutterClean'), cmd: 'flutter clean' },
    { label: t('ios.clean.step.derivedData'), cmd: 'rm -rf "$HOME/Library/Developer/Xcode/DerivedData/Runner-"*' },
    { label: t('ios.clean.step.buildDir'), cmd: 'rm -rf build ios/build' },
    { label: t('ios.clean.step.pubGet'), cmd: 'flutter pub get' },
    { label: t('ios.clean.step.pods'), cmd: 'cd ios && pod install' },
  ];

  const stepper = ui.makeStepper();
  for (const step of steps) {
    stepper.next(step.label);
    try {
      await execAsync(step.cmd, { cwd: projectDir, maxBuffer: 50 * 1024 * 1024, shell: '/bin/bash' });
    } catch (err) {
      stepper.fail(`${step.label}: ${err.message}`);
      throw err;
    }
  }
  stepper.succeed();
}

async function runReleaseScript(projectDir, args, t) {
  const scriptPath = path.join(projectDir, RELEASE_SCRIPT);
  if (!(await fs.pathExists(scriptPath))) {
    throw new Error(t('ios.error.noScript'));
  }

  const isBuildOnly = args.includes('--no-upload');
  const titleKey = isBuildOnly ? 'ios.build.task.building' : 'ios.release.task.building';
  const doneKey = isBuildOnly ? 'ios.build.task.done' : 'ios.release.task.done';
  const failKey = isBuildOnly ? 'ios.build.task.failed' : 'ios.release.task.failed';

  const spinner = ui.timedSpinner();
  spinner.start(t(titleKey));

  return new Promise((resolve, reject) => {
    const proc = spawn('bash', [scriptPath, ...args], {
      cwd: projectDir,
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    let allOutput = '';
    let uploadStage = false;

    // Trim noise so the spinner message stays readable. Skip empty lines,
    // pure dotted progress bars (e.g. "....."), and lines that are just
    // separators. Show the last meaningful line as the spinner status.
    const isMeaningful = (line) => {
      if (!line) return false;
      if (/^[.\s]+$/.test(line)) return false;
      if (/^=+$/.test(line)) return false;
      return true;
    };

    const handleChunk = (chunk) => {
      const text = chunk.toString();
      allOutput += text;

      if (!uploadStage && /Uploading to App Store Connect/i.test(text)) {
        uploadStage = true;
        spinner.message(t('ios.release.task.uploading'));
        return;
      }

      const lines = text.split('\n').map((l) => l.replace(/\r/g, '').trim()).filter(isMeaningful);
      const lastLine = lines[lines.length - 1];
      if (lastLine) spinner.message(lastLine);
    };

    proc.stdout.on('data', handleChunk);
    proc.stderr.on('data', handleChunk);

    proc.on('close', (code) => {
      if (code === 0) {
        spinner.stop(t(doneKey));
        resolve();
      } else {
        spinner.stop(t(failKey), 2);
        const error = new Error(allOutput || `release script exited with code ${code}`);
        error.buildOutput = allOutput;
        reject(error);
      }
    });

    proc.on('error', (err) => {
      spinner.stop(t(failKey), 2);
      reject(err);
    });
  });
}

module.exports = {
  APPLE_ENV_REL,
  APPLE_ENV_EXAMPLE,
  RELEASE_SCRIPT,
  URL_APP_STORE_CONNECT_APPS,
  URL_APP_STORE_CONNECT_API,
  bumpPubspecVersion,
  isKasyFlutterProject,
  readBundleId,
  applePrivateKeyPath,
  loadAppleEnv,
  validateAppleSetup,
  writeAppleEnv,
  installPrivateKey,
  openUrl,
  iosReleaseDocPath,
  runReleaseScript,
  checkIosSigning,
  MIN_DISK_GB_FOR_IOS_BUILD,
  getFreeDiskGb,
  checkDiskSpaceForIosBuild,
  isXcodeCacheBuildError,
  isPodNetworkError,
  printBuildFailureHints,
  printPodNetworkHints,
  runIosClean,
};
