import { NextResponse } from "next/server";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { join, dirname } from "path";
import type {
  InstalledPackage,
  InstalledResource,
  PackageFilterEntry,
  PiManifest,
  ResourceType,
} from "@/lib/package-types";

export const dynamic = "force-dynamic";

// ── Helpers ──────────────────────────────────────────────

function globalSettingsPath(): string {
  return join(homedir(), ".pi", "agent", "settings.json");
}

function projectSettingsPath(cwd: string): string {
  return join(cwd, ".pi", "settings.json");
}

function globalNpmRoot(): string {
  return join(homedir(), ".pi", "agent", "npm", "node_modules");
}

function projectNpmRoot(cwd: string): string {
  return join(cwd, ".pi", "npm", "node_modules");
}

function parseJsonFile<T>(path: string): T | null {
  try {
    if (!existsSync(path)) return null;
    return JSON.parse(readFileSync(path, "utf8")) as T;
  } catch {
    return null;
  }
}

function extractPackageName(source: string): string {
  // "npm:@scope/name@version" → "@scope/name"
  // "npm:name@version" → "name"
  // "git:github.com/user/repo" → "repo"
  // "/absolute/path" → basename of path
  if (source.startsWith("npm:")) {
    const withoutPrefix = source.slice(4);
    // Strip version
    const atIdx = withoutPrefix.lastIndexOf("@");
    if (atIdx > 0 && !withoutPrefix.startsWith("@")) {
      // Simple package: "name@version"
      return withoutPrefix.slice(0, atIdx);
    }
    if (atIdx === 1 && withoutPrefix.startsWith("@")) {
      // Scoped package: "@scope/name@version" - find the second @
      const secondAt = withoutPrefix.indexOf("@", 2);
      if (secondAt > 0) return withoutPrefix.slice(0, secondAt);
    }
    return withoutPrefix;
  }
  if (source.startsWith("git:")) {
    const parts = source.split("/");
    return parts[parts.length - 1].replace(/\.git$/, "");
  }
  // Local path
  const parts = source.split("/").filter(Boolean);
  return parts[parts.length - 1] || source;
}

function extractPackageFullName(source: string): string {
  // Extract the npm package name from "npm:@scope/name@version"
  if (source.startsWith("npm:")) {
    const withoutPrefix = source.slice(4);
    const atIdx = withoutPrefix.lastIndexOf("@");
    if (atIdx > 0 && !withoutPrefix.startsWith("@")) {
      return withoutPrefix.slice(0, atIdx);
    }
    if (atIdx === 1 && withoutPrefix.startsWith("@")) {
      const secondAt = withoutPrefix.indexOf("@", 2);
      if (secondAt > 0) return withoutPrefix.slice(0, secondAt);
    }
    return withoutPrefix;
  }
  return source;
}

function readPackageJson(installPath: string): { name?: string; version?: string; description?: string; pi?: PiManifest; repository?: { url?: string } | string; homepage?: string } | null {
  const pkgPath = join(installPath, "package.json");
  return parseJsonFile(pkgPath);
}

/** Normalize a git+https:// URL to a clean https:// URL the browser can open. */
function cleanUrl(url: string | undefined): string | undefined {
  if (!url) return undefined;
  let cleaned = url.replace(/^git\+(https?:\/\/)/, "$1");
  cleaned = cleaned.replace(/^git\+ssh:\/\/git@([^/]+)\/(.*)/, (_, host, path) => `https://${host}/${path}`);
  cleaned = cleaned.replace(/\.git$/, "");
  cleaned = cleaned.replace(/\/$/, "");
  return cleaned || undefined;
}

function findInstallPath(source: string, npmRoot: string): string | null {
  const name = extractPackageFullName(source);
  if (!name) return null;
  const candidate = join(npmRoot, name);
  if (existsSync(join(candidate, "package.json"))) return candidate;
  // Try scoped under node_modules
  if (name.startsWith("@")) {
    const [scope, pkg] = name.split("/");
    if (scope && pkg) {
      const alt = join(npmRoot, scope, pkg);
      if (existsSync(join(alt, "package.json"))) return alt;
    }
  }
  return null;
}

/**
 * Decide whether a single resource is enabled given the package's filter list
 * for its type. The filter list uses two syntaxes:
 *   - "-path" → this path is disabled (blacklist)
 *   - "path" or "+path" → opt-in whitelist entry; if any whitelist entry is
 *     present, only those paths are enabled
 * If no filter list is provided, the resource is enabled.
 */
function isResourceEnabled(filterPaths: string[] | undefined, p: string): boolean {
  if (!filterPaths) return true;
  // Explicit disable always wins.
  if (filterPaths.includes(`-${p}`)) return false;
  // Whitelist mode kicks in when the list contains at least one non-"-" entry.
  const hasWhitelist = filterPaths.some((f) => !f.startsWith("-"));
  if (hasWhitelist) {
    return filterPaths.includes(`+${p}`) || filterPaths.includes(p);
  }
  return true;
}

function loadPackageResources(pkgSource: string, piManifest: PiManifest | undefined, filters: Partial<PackageFilterEntry> | null): InstalledResource[] {
  const resources: InstalledResource[] = [];
  const filter = filters ?? {};

  const addResources = (paths: string[] | undefined, type: ResourceType) => {
    const filterPaths = filter[`${type}s` as keyof typeof filter] as string[] | undefined;
    for (const p of paths ?? []) {
      resources.push({ path: p, type, enabled: isResourceEnabled(filterPaths, p) });
    }
  };

  addResources(piManifest?.extensions, "extension");
  addResources(piManifest?.skills, "skill");
  addResources(piManifest?.prompts, "prompt");
  addResources(piManifest?.themes, "theme");

  return resources;
}

function parsePackageEntry(entry: string | Record<string, unknown>): { source: string; filter: Partial<PackageFilterEntry> | null } {
  if (typeof entry === "string") {
    return { source: entry, filter: null };
  }
  const e = entry as Record<string, unknown>;
  return {
    source: String(e.source ?? ""),
    filter: (e.extensions || e.skills || e.prompts || e.themes)
      ? {
          source: String(e.source ?? ""),
          extensions: e.extensions as string[] | undefined,
          skills: e.skills as string[] | undefined,
          prompts: e.prompts as string[] | undefined,
          themes: e.themes as string[] | undefined,
        }
      : null,
  };
}

function loadInstalledPackagesFromScope(
  packages: unknown[],
  scope: "global" | "project",
  npmRoot: string,
): InstalledPackage[] {
  const result: InstalledPackage[] = [];
  for (const entry of packages ?? []) {
    if (typeof entry !== "string" && typeof entry !== "object") continue;
    const { source, filter } = parsePackageEntry(entry as string | Record<string, unknown>);
    if (!source) continue;

    const name = extractPackageName(source);
    const installPath = findInstallPath(source, npmRoot);
    const pkgJson = installPath ? readPackageJson(installPath) : null;

    const piManifest = pkgJson?.pi;
    const resources = loadPackageResources(source, piManifest, filter);

    // Extract repo URL from package.json (it can be a string or an object with .url)
    const repoRaw = pkgJson?.repository;
    const repoUrl = typeof repoRaw === "string" ? cleanUrl(repoRaw) : cleanUrl((repoRaw as { url?: string })?.url);

    result.push({
      source,
      name: pkgJson?.name ?? name,
      version: pkgJson?.version,
      description: pkgJson?.description,
      scope,
      installPath: installPath ?? undefined,
      resources,
      piManifest: piManifest ?? undefined,
      repository: repoUrl,
      homepage: cleanUrl(pkgJson?.homepage),
    });
  }
  return result;
}

// ── Route handlers ──────────────────────────────────────

// GET /api/packages?cwd=<path>
// List installed packages from global and project settings
export async function GET(req: Request) {
  try {
    const { searchParams } = new URL(req.url);
    const cwd = searchParams.get("cwd") || undefined;

    const allPackages: InstalledPackage[] = [];

    // Global packages
    const globalSettings = parseJsonFile<{ packages?: unknown[] }>(globalSettingsPath());
    if (globalSettings?.packages) {
      const globalPkgs = loadInstalledPackagesFromScope(
        globalSettings.packages,
        "global",
        globalNpmRoot(),
      );
      allPackages.push(...globalPkgs);
    }

    // Project packages
    if (cwd) {
      const projSettings = parseJsonFile<{ packages?: unknown[] }>(projectSettingsPath(cwd));
      if (projSettings?.packages) {
        const projPkgs = loadInstalledPackagesFromScope(
          projSettings.packages,
          "project",
          projectNpmRoot(cwd),
        );
        allPackages.push(...projPkgs);
      }
    }

    return NextResponse.json({ packages: allPackages });
  } catch (e: unknown) {
    return NextResponse.json({ error: String(e) }, { status: 500 });
  }
}

// PATCH /api/packages — toggle a resource within a package
// body: {
//   source: string;
//   resourcePath: string;
//   resourceType: ResourceType;
//   enabled: boolean;
//   scope?: "global" | "project"; // required to disambiguate; defaults to "global"
//   cwd?: string;                  // required when scope === "project"
// }
//
// This adds/removes the resource from the package filter in settings.json.
// If no filter entry exists for the package, it creates one.
export async function PATCH(req: Request) {
  try {
    const body = await req.json() as {
      source: string;
      resourcePath: string;
      resourceType: ResourceType;
      enabled: boolean;
      scope?: "global" | "project";
      cwd?: string;
    };

    const { source, resourcePath, resourceType, enabled, cwd } = body;
    if (!source || !resourcePath || !resourceType) {
      return NextResponse.json({ error: "source, resourcePath, and resourceType required" }, { status: 400 });
    }

    // Trust the caller's declared scope. Project scope requires cwd; missing
    // settings files are a 404, not a silent fallback into the other scope.
    const scope: "global" | "project" = body.scope === "project" ? "project" : "global";

    let settingsPath: string;
    if (scope === "project") {
      if (!cwd) {
        return NextResponse.json({ error: "cwd required for project scope" }, { status: 400 });
      }
      settingsPath = projectSettingsPath(cwd);
    } else {
      settingsPath = globalSettingsPath();
    }

    if (!existsSync(settingsPath)) {
      return NextResponse.json(
        { error: `settings file not found: ${settingsPath}` },
        { status: 404 },
      );
    }

    const raw = readFileSync(settingsPath, "utf8");
    const settings = JSON.parse(raw);
    const packages: unknown[] = settings.packages ?? [];

    // Find existing entry for this source
    let found = false;
    const typeKey = `${resourceType}s` as keyof PackageFilterEntry; // "extensions" | "skills" | "prompts" | "themes"

    for (let i = 0; i < packages.length; i++) {
      const entry = packages[i];
      const entrySource = typeof entry === "string" ? entry : (entry as Record<string, unknown>).source;
      if (entrySource !== source) continue;

      found = true;

      if (typeof entry === "string") {
        // Upgrade from string to filter object
        const filterEntry: Record<string, unknown> = { source };
        const existingList = filterEntry[typeKey] as string[] | undefined;
        if (enabled) {
          if (existingList) {
            const filtered = existingList.filter((f: string) => f !== `-${resourcePath}` && f !== resourcePath);
            if (!filtered.some((f: string) => f === `+${resourcePath}` || f === resourcePath)) {
              filtered.push(resourcePath);
            }
            filterEntry[typeKey] = filtered;
          } else {
            filterEntry[typeKey] = [resourcePath];
          }
        } else {
          if (existingList) {
            if (!existingList.some((f: string) => f === `-${resourcePath}`)) {
              filterEntry[typeKey] = [...existingList, `-${resourcePath}`];
            }
          } else {
            filterEntry[typeKey] = [`-${resourcePath}`];
          }
        }
        packages[i] = filterEntry;
      } else {
        // Already a filter object
        const filterEntry = entry as Record<string, unknown>;
        const currentList = (filterEntry[typeKey] as string[]) ?? [];
        if (enabled) {
          // Remove from exclude list
          filterEntry[typeKey] = currentList.filter(
            (f) => f !== `-${resourcePath}` && f !== resourcePath,
          );
          // If list is empty, delete the key (meaning "allow all")
          if ((filterEntry[typeKey] as string[]).length === 0) {
            delete filterEntry[typeKey];
          }
        } else {
          // Add to exclude list if not already there
          if (!currentList.includes(`-${resourcePath}`) && !currentList.includes(resourcePath)) {
            filterEntry[typeKey] = [...currentList, `-${resourcePath}`];
          }
        }
        packages[i] = filterEntry;
      }
      break;
    }

    if (!found) {
      // Create new filter entry
      const filterEntry: Record<string, unknown> = { source };
      filterEntry[typeKey] = enabled ? [resourcePath] : [`-${resourcePath}`];
      packages.push(filterEntry);
    }

    settings.packages = packages;
    writeFileSync(settingsPath, JSON.stringify(settings, null, 4), "utf8");

    return NextResponse.json({ success: true });
  } catch (e: unknown) {
    return NextResponse.json({ error: String(e) }, { status: 500 });
  }
}
