import { execSync, type StdioOptions } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

// Submodules skipped during worktree setup (shared via symlinks or pre-built)
const SKIPPED_SUBMODULES = [
  'apps/readest-app/.claude/skills/gstack', // shared via .claude symlink
  'packages/simplecc-wasm', // built assets already in public/vendor
];

const arg = process.argv[2];
if (!arg) {
  console.error('Usage: pnpm worktree:new <branch-name|pr-number>');
  process.exit(1);
}

const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();

// Git output goes to stderr so stdout carries only the path (enables: cd $(pnpm worktree:new <arg>))
const gitStdio: StdioOptions = ['inherit', process.stderr, process.stderr];

// Fetch origin so origin/main is up to date
console.error('--- Fetching origin ---');
execSync('git fetch origin', { stdio: gitStdio, cwd: repoRoot });

let localBranch: string;
let worktreePath: string;

if (/^\d+$/.test(arg)) {
  // PR number -- fetch and set up remote tracking so `git push` works (even for forks)
  localBranch = `pr-${arg}`;
  worktreePath = path.join(path.dirname(repoRoot), `readest-${localBranch}`);

  // Get PR metadata to determine the source repo and branch
  const prJson = execSync(
    `gh pr view ${arg} --json headRefName,headRepositoryOwner,headRepository`,
    {
      encoding: 'utf8',
      cwd: repoRoot,
    },
  );
  const pr = JSON.parse(prJson) as {
    headRefName: string;
    headRepositoryOwner: { login: string };
    headRepository: { name: string };
  };
  const forkOwner = pr.headRepositoryOwner.login;
  const forkRepo = pr.headRepository.name;
  const remoteBranch = pr.headRefName;

  // Use "origin" if the PR is from the same repo, otherwise add the fork as a remote
  const originUrl = execSync('git remote get-url origin', {
    encoding: 'utf8',
    cwd: repoRoot,
  }).trim();
  const isFromOrigin = originUrl.includes(`/${forkOwner}/${forkRepo}`);
  const remoteName = isFromOrigin ? 'origin' : forkOwner;

  if (!isFromOrigin) {
    try {
      execSync(`git remote get-url "${remoteName}"`, { encoding: 'utf8', cwd: repoRoot });
    } catch {
      execSync(`git remote add "${remoteName}" "https://github.com/${forkOwner}/${forkRepo}.git"`, {
        stdio: gitStdio,
        cwd: repoRoot,
      });
    }
  }

  execSync(`git fetch "${remoteName}" "${remoteBranch}:${localBranch}"`, {
    stdio: gitStdio,
    cwd: repoRoot,
  });
  execSync(`git worktree add "${worktreePath}" "${localBranch}"`, {
    stdio: gitStdio,
    cwd: repoRoot,
  });

  // Set upstream so `git push` targets the correct fork and branch.
  // Use git-config directly instead of `branch --set-upstream-to` because
  // the targeted fetch above doesn't create a remote-tracking ref.
  execSync(`git -C "${worktreePath}" config "branch.${localBranch}.remote" "${remoteName}"`);
  execSync(
    `git -C "${worktreePath}" config "branch.${localBranch}.merge" "refs/heads/${remoteBranch}"`,
  );
} else {
  // Branch name -- slashes replaced with dashes for the directory name
  localBranch = arg;
  worktreePath = path.join(path.dirname(repoRoot), `readest-${arg.replace(/\//g, '-')}`);

  if (fs.existsSync(worktreePath)) {
    console.error(`Worktree path already exists: ${worktreePath}`);
    console.error('Removing existing worktree...');
    // Deinit only submodules we manage — skipped ones were never initialized
    const initedSubs = execSync(
      'git config --file .gitmodules --get-regexp "submodule\\..*\\.path"',
      { encoding: 'utf8', cwd: worktreePath },
    )
      .trim()
      .split('\n')
      .map((line) => line.split(/\s+/)[1]!)
      .filter((p) => !SKIPPED_SUBMODULES.includes(p));
    for (const sub of initedSubs) {
      execSync(`git -C "${worktreePath}" submodule deinit --force -- "${sub}"`, {
        stdio: gitStdio,
        cwd: repoRoot,
      });
    }
    execSync(`git worktree remove --force "${worktreePath}"`, { stdio: gitStdio, cwd: repoRoot });
  }

  // Check if the branch already exists
  const branchExists = execSync('git branch --list --format="%(refname:short)"', {
    encoding: 'utf8',
    cwd: repoRoot,
  })
    .split('\n')
    .includes(localBranch);

  if (branchExists) {
    execSync(`git worktree add "${worktreePath}" "${localBranch}"`, {
      stdio: gitStdio,
      cwd: repoRoot,
    });
  } else {
    execSync(`git worktree add -b "${localBranch}" "${worktreePath}" origin/main`, {
      stdio: gitStdio,
      cwd: repoRoot,
    });
  }
}

// Rebase onto origin/main so the worktree starts from the latest upstream
console.error('\n--- Rebasing onto origin/main ---');
execSync('git rebase origin/main', { stdio: gitStdio, cwd: worktreePath });

// Repoint submodule URLs to local .git/modules/ clones to avoid remote fetches.
// Submodules without a local cache fall back to the remote URL.
console.error('\n--- Initializing submodules (using local objects) ---');
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8', cwd: repoRoot }).trim();
const absGitDir = path.resolve(repoRoot, gitDir);
const submoduleNames = execSync(
  'git config --file .gitmodules --get-regexp "submodule\\..*\\.path"',
  { encoding: 'utf8', cwd: worktreePath },
)
  .trim()
  .split('\n')
  .map((line) => {
    // line: submodule.<name>.path <path>
    const match = line.match(/^submodule\.(.+)\.path\s+(.+)$/);
    return { name: match![1]!, subPath: match![2]! };
  })
  .filter(({ subPath }) => !SKIPPED_SUBMODULES.includes(subPath));

for (const { name, subPath } of submoduleNames) {
  const localModuleDir = path.join(absGitDir, 'modules', subPath);
  // Also check if the submodule has a full .git/ directory (cloned outside of git's modules cache)
  const subGitDir = path.join(repoRoot, subPath, '.git');
  let localDir: string | undefined;
  if (fs.existsSync(localModuleDir)) {
    localDir = localModuleDir;
    console.error(`  ${subPath} -> local (.git/modules)`);
  } else if (fs.existsSync(subGitDir)) {
    localDir = fs.statSync(subGitDir).isDirectory()
      ? subGitDir
      : path.resolve(
          path.join(repoRoot, subPath),
          fs.readFileSync(subGitDir, 'utf8').replace('gitdir: ', '').trim(),
        );
    console.error(`  ${subPath} -> local (standalone .git)`);
  } else {
    console.error(`  ${subPath} -> remote (no local cache)`);
  }
  if (localDir) {
    // Allow fetching any commit (not just branch tips) from the local source
    execSync(`git -C "${localDir}" config uploadpack.allowAnySHA1InWant true`);
    execSync(`git -C "${worktreePath}" config "submodule.${name}.url" "${localDir}"`);
  }
}

for (const { subPath } of submoduleNames) {
  execSync(
    `git -c protocol.file.allow=always submodule update --init --recursive -- "${subPath}"`,
    { stdio: gitStdio, cwd: worktreePath },
  );
}

// Restore original remote URLs so `git push` in submodules works correctly
for (const { name } of submoduleNames) {
  const origUrl = execSync(`git config --file .gitmodules "submodule.${name}.url"`, {
    encoding: 'utf8',
    cwd: worktreePath,
  }).trim();
  execSync(`git -C "${worktreePath}" config "submodule.${name}.url" "${origUrl}"`);
}

// Install dependencies
console.error('\n--- Installing dependencies ---');
execSync('pnpm install', { stdio: gitStdio, cwd: worktreePath });

// Copy .env* files from the app directory to the new worktree's app directory
const appRelPath = 'apps/readest-app';
const srcAppDir = path.join(repoRoot, appRelPath);
const dstAppDir = path.join(worktreePath, appRelPath);
const envFiles = fs.readdirSync(srcAppDir).filter((f) => f.startsWith('.env'));
if (envFiles.length > 0) {
  console.error(`\n--- Copying ${envFiles.length} .env* files ---`);
  for (const envFile of envFiles) {
    const src = path.join(srcAppDir, envFile);
    const dst = path.join(dstAppDir, envFile);
    if (!fs.existsSync(dst)) {
      fs.copyFileSync(src, dst);
      console.error(`  ${envFile}`);
    }
  }
}

// Symlink target so the worktree shares the Rust build cache
const srcTarget = path.join(repoRoot, 'target');
const dstTarget = path.join(worktreePath, 'target');
if (fs.existsSync(srcTarget) && !fs.existsSync(dstTarget)) {
  console.error('\n--- Symlinking src-tauri/target ---');
  fs.symlinkSync(srcTarget, dstTarget, 'junction');
}

// Initialize Tauri Android gen directory (needs platform-specific paths regenerated)
const genDir = path.join(dstAppDir, 'src-tauri', 'gen');
const androidGenDir = path.join(genDir, 'android');
if (fs.existsSync(androidGenDir)) {
  console.error('\n--- Initializing Tauri Android ---');
  fs.rmSync(androidGenDir, { recursive: true });
  execSync('pnpm tauri android init', { stdio: gitStdio, cwd: dstAppDir });
  execSync('pnpm tauri icon ../../data/icons/readest-book.png', {
    stdio: gitStdio,
    cwd: dstAppDir,
  });
  execSync(`git checkout ${appRelPath}/src-tauri/gen/android ${appRelPath}/src-tauri/icons`, {
    stdio: gitStdio,
    cwd: worktreePath,
  });
}

// Copy Tauri iOS gen from the bare repo, skipping rebuildable artifacts so
// the worktree owns its own Xcode project. `project.yml` and the Xcode build
// script walk up to `../../src` / `../../Cargo.toml`, which now resolve to
// the worktree's Rust source instead of the bare repo's — so `pnpm tauri ios
// dev` from a worktree actually builds the worktree's code. Symlinking
// silently routed every iOS build through the bare repo, even when running
// `pnpm tauri ios dev` from a worktree path.
//
// Skipped: `build/` (~300 MB of Xcode derived output), `Externals/<arch>/
// {debug,release}/` (Rust static libs — rebuilt against the worktree's
// src-tauri), per-user Xcode state, and `Pods/` (defensive; Readest's iOS
// uses SPM, not Cocoapods).
const appleGenDir = path.join(genDir, 'apple');
const srcAppleGen = path.join(srcAppDir, 'src-tauri', 'gen', 'apple');
if (fs.existsSync(srcAppleGen) && !fs.existsSync(appleGenDir)) {
  console.error('\n--- Copying Tauri iOS gen ---');
  fs.cpSync(srcAppleGen, appleGenDir, {
    recursive: true,
    dereference: false,
    filter: (src) => {
      const rel = path.relative(srcAppleGen, src);
      if (rel === 'build' || rel.startsWith('build/')) return false;
      if (/^Externals\/[^/]+\/(debug|release)(\/|$)/.test(rel)) return false;
      if (rel.endsWith('/xcuserdata') || rel.includes('/xcuserdata/')) return false;
      if (rel.endsWith('.xcuserstate')) return false;
      if (rel === 'Pods' || rel.startsWith('Pods/')) return false;
      return true;
    },
  });
}

// Symlink the remaining shared gen dirs (schemas are generated at build time
// and don't differ per branch; keystore.properties is signing config that
// must stay identical across worktrees to produce matching APKs).
for (const sub of ['schemas', 'android/keystore.properties']) {
  const src = path.join(srcAppDir, 'src-tauri', 'gen', sub);
  const dst = path.join(genDir, sub);
  if (fs.existsSync(src) && !fs.existsSync(dst)) {
    console.error(`  Symlinking src-tauri/gen/${sub}`);
    fs.symlinkSync(src, dst, 'junction');
  }
}

// Copy public/vendor to the new worktree (built assets not in git)
const srcVendor = path.join(srcAppDir, 'public', 'vendor');
const dstVendor = path.join(dstAppDir, 'public', 'vendor');
if (fs.existsSync(srcVendor) && !fs.existsSync(dstVendor)) {
  console.error('\n--- Copying public/vendor ---');
  fs.cpSync(srcVendor, dstVendor, { recursive: true });
}

// Print path to stdout -- allows: cd $(pnpm worktree:new <arg>)
process.stdout.write(worktreePath + '\n');
