/**
 * jscodeshift transform: rewrite `PageProps<T>` from `@akinon/next/types`
 * into `AsyncPageProps<T>` or `ResolvedPageProps<T>` per usage context.
 *
 * Decision tree per TSTypeReference:
 *   1. Enclosing function is `generateMetadata` → AsyncPageProps (always)
 *   2. File has `'use client'` directive → AsyncPageProps (always)
 *   3. Body uses `await props.params` / `use(props.params)` → AsyncPageProps
 *   4. Otherwise (server component, withSegmentDefaults) → ResolvedPageProps
 *
 * Additionally for client components (rule 2):
 *   - If the parameter is destructured (`({ params })`), refactor to
 *     `(props: AsyncPageProps<T>)` and inject `use()` calls at the top
 *     of the function body. This avoids the broken pattern where `params`
 *     is destructured synchronously but is actually a Promise in Next 16.
 *   - Only refactors when the destructure contains only `params` and/or
 *     `searchParams` (no other properties / rest spreads).
 *   - Idempotent: skipped if `use(props.params)` already exists in the body.
 *   - Adds `import { use } from 'react'` if not already present.
 *
 * Import is rewritten: PageProps removed, AsyncPageProps / ResolvedPageProps
 * added as needed, sorted alphabetically.
 */

'use strict';

const TARGET_IMPORT = '@akinon/next/types';

// ─── helpers ────────────────────────────────────────────────────────────────

function fileIsClient(source) {
  return /^\s*['"]use client['"]/.test(source);
}

function getEnclosingFunction(path) {
  let cur = path;
  while (cur.parent) {
    cur = cur.parent;
    const t = cur.node && cur.node.type;
    if (
      t === 'FunctionDeclaration' ||
      t === 'FunctionExpression' ||
      t === 'ArrowFunctionExpression'
    ) {
      return cur;
    }
  }
  return null;
}

function getFunctionName(fnPath) {
  const node = fnPath.node;
  if (node.id && node.id.name) return node.id.name;
  let p = fnPath.parent;
  while (p) {
    const t = p.node && p.node.type;
    if (t === 'VariableDeclarator' && p.node.id && p.node.id.name) {
      return p.node.id.name;
    }
    if (t === 'Property' || t === 'ObjectProperty' || t === 'MethodDefinition') {
      const key = p.node.key;
      if (key && key.name) return key.name;
    }
    if (
      t === 'FunctionDeclaration' ||
      t === 'FunctionExpression' ||
      t === 'ArrowFunctionExpression'
    ) {
      break;
    }
    p = p.parent;
  }
  return null;
}

function isParamsMember(node) {
  return (
    node &&
    node.type === 'MemberExpression' &&
    node.property &&
    (node.property.name === 'params' || node.property.name === 'searchParams')
  );
}

function bodyUsesAsyncResolve(fnPath, j) {
  let found = false;
  j(fnPath)
    .find(j.AwaitExpression)
    .forEach((p) => {
      if (isParamsMember(p.node.argument)) found = true;
    });
  j(fnPath)
    .find(j.CallExpression, { callee: { name: 'use' } })
    .forEach((p) => {
      const arg = p.node.arguments && p.node.arguments[0];
      if (isParamsMember(arg)) found = true;
    });
  return found;
}

// ─── destructure analysis ────────────────────────────────────────────────────

/**
 * Analyse the first parameter of a function.
 * Returns null if the param is not an ObjectPattern that contains only
 * params / searchParams (with optional nested destructuring).
 *
 * On success returns:
 *   {
 *     paramsPattern:       AST node for the value of the `params` property,
 *                          or null if not present.
 *     searchParamsPattern: same for `searchParams`.
 *   }
 */
function analyseDestructure(fnPath) {
  const params = fnPath.node.params;
  if (!params || params.length !== 1) return null;

  const p = params[0];
  if (p.type !== 'ObjectPattern') return null;

  // Reject if any property is a rest element or something other than
  // params / searchParams.
  let paramsPattern = null;
  let searchParamsPattern = null;

  for (const prop of p.properties) {
    // Rest element: e.g. `...rest`
    if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
      return null;
    }
    if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') {
      return null;
    }
    const keyName = (prop.key && prop.key.name) || null;
    if (keyName === 'params') {
      paramsPattern = prop.value;
    } else if (keyName === 'searchParams') {
      searchParamsPattern = prop.value;
    } else {
      // Any other property → not safe to refactor automatically.
      return null;
    }
  }

  if (!paramsPattern && !searchParamsPattern) return null;

  return { paramsPattern, searchParamsPattern };
}

// ─── destructure refactor ────────────────────────────────────────────────────

/**
 * For a client-component function whose first param is destructured as
 * `({ params, ... })`, rewrite to `(props: AsyncPageProps<T>)` and insert
 * `const x = use(props.params)` at the top of the body.
 *
 * Does nothing if the body already has `use(props.params)` (idempotent).
 */
function refactorDestructure(fnPath, info, j) {
  // Idempotency: bail if already refactored
  if (bodyUsesAsyncResolve(fnPath, j)) return false;

  const body = fnPath.node.body;
  if (!body || body.type !== 'BlockStatement') return false;

  const statements = [];

  const makeUseCall = (propName) =>
    j.callExpression(j.identifier('use'), [
      j.memberExpression(j.identifier('props'), j.identifier(propName))
    ]);

  if (info.paramsPattern) {
    statements.push(
      j.variableDeclaration('const', [
        j.variableDeclarator(info.paramsPattern, makeUseCall('params'))
      ])
    );
  }
  if (info.searchParamsPattern) {
    statements.push(
      j.variableDeclaration('const', [
        j.variableDeclarator(info.searchParamsPattern, makeUseCall('searchParams'))
      ])
    );
  }

  // Replace the first parameter with a plain `props` identifier.
  // The type annotation will be set separately (by the type-rename step).
  const firstParam = fnPath.node.params[0];
  const propsId = j.identifier('props');
  // Carry over the TSTypeAnnotation that the ObjectPattern had (if any).
  if (firstParam.typeAnnotation) {
    propsId.typeAnnotation = firstParam.typeAnnotation;
  }
  fnPath.node.params[0] = propsId;

  // Insert use() calls at the start of the block.
  body.body.unshift(...statements);

  return true;
}

// ─── `use` import ────────────────────────────────────────────────────────────

function ensureUseImport(root, j) {
  const reactImports = root.find(j.ImportDeclaration, {
    source: { value: 'react' }
  });

  if (reactImports.length === 0) {
    // Insert a new import before the first import declaration.
    const firstImport = root.find(j.ImportDeclaration).at(0);
    if (firstImport.length) {
      firstImport.insertBefore(
        j.importDeclaration(
          [j.importSpecifier(j.identifier('use'))],
          j.literal('react')
        )
      );
    }
    return;
  }

  let hasUse = false;
  reactImports.forEach((imp) => {
    const specs = imp.node.specifiers || [];
    if (
      specs.some(
        (s) => s.type === 'ImportSpecifier' && s.imported && s.imported.name === 'use'
      )
    ) {
      hasUse = true;
    }
  });

  if (!hasUse) {
    reactImports.at(0).node.specifiers.push(
      j.importSpecifier(j.identifier('use'))
    );
  }
}

// ─── main transform ──────────────────────────────────────────────────────────

function transform(fileInfo, api) {
  const j = api.jscodeshift;
  const root = j(fileInfo.source);
  const isClient = fileIsClient(fileInfo.source);

  const akinonImports = root.find(j.ImportDeclaration, {
    source: { value: TARGET_IMPORT }
  });
  if (akinonImports.length === 0) return null;

  let hasPageProps = false;
  akinonImports.forEach((imp) => {
    if (
      (imp.node.specifiers || []).some(
        (s) =>
          s.type === 'ImportSpecifier' &&
          s.imported &&
          s.imported.name === 'PageProps'
      )
    ) {
      hasPageProps = true;
    }
  });
  if (!hasPageProps) return null;

  let usedAsync = false;
  let usedResolved = false;
  let needUseImport = false;

  // Collect all functions that need destructure refactoring so we do it
  // after all type-renames (avoids mutating the AST under the iterator).
  const toRefactor = []; // Array<{ fnPath, info }>

  root
    .find(j.TSTypeReference, {
      typeName: { type: 'Identifier', name: 'PageProps' }
    })
    .forEach((path) => {
      const fnPath = getEnclosingFunction(path);
      let target = 'AsyncPageProps';

      if (fnPath) {
        const fnName = getFunctionName(fnPath);
        if (fnName === 'generateMetadata') {
          target = 'AsyncPageProps';
        } else if (isClient) {
          target = 'AsyncPageProps';
          // Collect for destructure refactor (if applicable).
          const info = analyseDestructure(fnPath);
          if (info) {
            toRefactor.push({ fnPath, info });
          }
        } else if (bodyUsesAsyncResolve(fnPath, j)) {
          target = 'AsyncPageProps';
        } else {
          target = 'ResolvedPageProps';
        }
      }

      path.node.typeName = j.identifier(target);
      if (target === 'AsyncPageProps') usedAsync = true;
      else usedResolved = true;
    });

  // Perform destructure refactors (deferred to avoid iterator invalidation).
  for (const { fnPath, info } of toRefactor) {
    const didRefactor = refactorDestructure(fnPath, info, j);
    if (didRefactor) needUseImport = true;
  }

  if (needUseImport) {
    ensureUseImport(root, j);
  }

  // Update @akinon/next/types import.
  akinonImports.forEach((imp) => {
    const specs = imp.node.specifiers || [];
    const existing = new Set(
      specs
        .filter((s) => s.type === 'ImportSpecifier' && s.imported)
        .map((s) => s.imported.name)
    );
    const next = specs.filter(
      (s) =>
        !(
          s.type === 'ImportSpecifier' &&
          s.imported &&
          s.imported.name === 'PageProps'
        )
    );
    if (usedAsync && !existing.has('AsyncPageProps')) {
      next.push(j.importSpecifier(j.identifier('AsyncPageProps')));
    }
    if (usedResolved && !existing.has('ResolvedPageProps')) {
      next.push(j.importSpecifier(j.identifier('ResolvedPageProps')));
    }
    next.sort((a, b) => {
      if (a.type !== 'ImportSpecifier' || b.type !== 'ImportSpecifier') return 0;
      return a.imported.name.localeCompare(b.imported.name);
    });
    imp.node.specifiers = next;
  });

  const output = root.toSource({ quote: 'single' });
  // jscodeshift occasionally emits 'use client';; (double semi) when
  // the directive is the first token — strip the duplicate.
  return output.replace(/^(['"]use client['"]);;/m, '$1;');
}

module.exports = transform;
