import type {
  IntegrationProvider,
  CardData,
  TestResult,
  PushResult,
  ExternalField,
  JsonValue,
} from "../types";
import { prisma } from "@/lib/db";

const PCO_API_BASE = "https://api.planningcenteronline.com/people/v2";
const PCO_TOKEN_URL = "https://api.planningcenteronline.com/oauth/token";
// PCO requires a descriptive User-Agent or it may return 403
const PCO_USER_AGENT = "EchoOCR/1.0 (+https://echoocr.com)";
// Refresh a token if it is within 5 minutes of expiring
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;

interface PcoConfig {
  accessToken: string;
  refreshToken?: string;
  tokenExpiresAt?: number;
  defaultListId?: string;
  defaultWorkflowId?: string;
  matchStrategy?: "email_first" | "name_first" | "manual";
  // Injected by dispatch for token persistence — not user-facing
  _integrationId?: string;
}

function parseConfig(config: JsonValue): PcoConfig {
  const c = (config as Record<string, unknown>) || {};
  return {
    accessToken: (c.accessToken as string) || "",
    refreshToken: (c.refreshToken as string) || undefined,
    tokenExpiresAt: (c.tokenExpiresAt as number) || undefined,
    defaultListId: (c.defaultListId as string) || undefined,
    defaultWorkflowId: (c.defaultWorkflowId as string) || undefined,
    matchStrategy:
      (c.matchStrategy as PcoConfig["matchStrategy"]) || "email_first",
    _integrationId: (c._integrationId as string) || undefined,
  };
}

async function refreshAccessToken(
  refreshToken: string,
  integrationId?: string
): Promise<{ accessToken: string; refreshToken: string; expiresAt: number }> {
  const clientId = process.env.PCO_CLIENT_ID;
  const clientSecret = process.env.PCO_CLIENT_SECRET;
  if (!clientId || !clientSecret) {
    throw new Error(
      "Planning Center OAuth is not configured on the server (missing PCO_CLIENT_ID / PCO_CLIENT_SECRET)"
    );
  }

  const res = await fetch(PCO_TOKEN_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "User-Agent": PCO_USER_AGENT,
    },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(
      `Failed to refresh Planning Center token (${res.status}): ${text}`
    );
  }

  const tokens = await res.json();
  const expiresAt = Date.now() + (tokens.expires_in ?? 7200) * 1000;
  const newAccess = tokens.access_token as string;
  const newRefresh = (tokens.refresh_token as string) || refreshToken;

  if (integrationId) {
    try {
      const integration = await prisma.integration.findUnique({
        where: { id: integrationId },
      });
      if (integration) {
        const existing = (integration.config as Record<string, unknown>) || {};
        await prisma.integration.update({
          where: { id: integrationId },
          data: {
            config: {
              ...existing,
              accessToken: newAccess,
              refreshToken: newRefresh,
              tokenExpiresAt: expiresAt,
            },
          },
        });
      }
    } catch (err) {
      console.error("[pco] Failed to persist refreshed token:", err);
    }
  }

  return {
    accessToken: newAccess,
    refreshToken: newRefresh,
    expiresAt,
  };
}

async function ensureFreshToken(cfg: PcoConfig): Promise<string> {
  const needsRefresh =
    cfg.tokenExpiresAt !== undefined &&
    cfg.tokenExpiresAt - Date.now() < TOKEN_REFRESH_BUFFER_MS;

  if (!needsRefresh) return cfg.accessToken;
  if (!cfg.refreshToken) return cfg.accessToken;

  try {
    const refreshed = await refreshAccessToken(
      cfg.refreshToken,
      cfg._integrationId
    );
    cfg.accessToken = refreshed.accessToken;
    cfg.refreshToken = refreshed.refreshToken;
    cfg.tokenExpiresAt = refreshed.expiresAt;
    return refreshed.accessToken;
  } catch (err) {
    console.error("[pco] Token refresh failed:", err);
    return cfg.accessToken;
  }
}

async function pcoFetch(
  cfg: PcoConfig,
  path: string,
  options: RequestInit = {},
  retriesLeft = 2
): Promise<{
  data?: unknown;
  included?: unknown;
  meta?: { total_count?: number; [k: string]: unknown };
  [k: string]: unknown;
}> {
  const accessToken = await ensureFreshToken(cfg);

  const res = await fetch(`${PCO_API_BASE}${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
      "User-Agent": PCO_USER_AGENT,
      ...(options.headers || {}),
    },
  });

  // Handle rate limiting (429) with retry
  if (res.status === 429 && retriesLeft > 0) {
    const retryAfter =
      Number(res.headers.get("Retry-After")) ||
      Number(res.headers.get("X-PCO-API-Request-Rate-Period")) ||
      20;
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return pcoFetch(cfg, path, options, retriesLeft - 1);
  }

  // Handle expired token (401) by forcing a refresh and retrying once
  if (res.status === 401 && retriesLeft > 0 && cfg.refreshToken) {
    try {
      const refreshed = await refreshAccessToken(
        cfg.refreshToken,
        cfg._integrationId
      );
      cfg.accessToken = refreshed.accessToken;
      cfg.refreshToken = refreshed.refreshToken;
      cfg.tokenExpiresAt = refreshed.expiresAt;
      return pcoFetch(cfg, path, options, retriesLeft - 1);
    } catch {
      // fall through to error
    }
  }

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`PCO API ${res.status}: ${text}`);
  }
  return res.json();
}

async function findPersonByEmail(
  cfg: PcoConfig,
  email: string
): Promise<string | null> {
  const data = await pcoFetch(
    cfg,
    `/emails?where[address]=${encodeURIComponent(email)}&include=person`
  );
  const included = (data.included as Array<{ id: string }> | undefined) || [];
  if (included.length > 0) return included[0].id;
  return null;
}

async function findPersonByName(
  cfg: PcoConfig,
  firstName: string,
  lastName: string
): Promise<string | null> {
  const data = await pcoFetch(
    cfg,
    `/people?where[first_name]=${encodeURIComponent(firstName)}&where[last_name]=${encodeURIComponent(lastName)}`
  );
  const rows = (data.data as Array<{ id: string }> | undefined) || [];
  if (rows.length > 0) return rows[0].id;
  return null;
}

function splitName(name: string): { firstName: string; lastName: string } {
  const parts = name.trim().split(/\s+/);
  if (parts.length === 1) {
    return { firstName: parts[0], lastName: "" };
  }
  return {
    firstName: parts[0],
    lastName: parts.slice(1).join(" "),
  };
}

// Maps Echo OCR card fields → PCO person attribute keys
const BUILT_IN_PERSON_FIELDS = new Set([
  "first_name",
  "last_name",
  "gender",
  "birthdate",
  "membership",
  "status",
  "middle_name",
  "nickname",
  "given_name",
  "anniversary",
  "grade",
  "graduation_year",
  "medical_notes",
  "child",
]);

export const planningCenterProvider: IntegrationProvider = {
  id: "planning_center",
  name: "Planning Center",
  description: "Sync people and response cards to Planning Center Online",
  icon: "planning_center",
  category: "chms",
  supportsFieldMapping: true,
  supportsOAuth: true,
  configFields: [
    {
      key: "defaultListId",
      label: "Default List ID (optional)",
      type: "text",
      helpText: "Add matched/created people to this PCO List",
    },
    {
      key: "defaultWorkflowId",
      label: "Default Workflow ID (optional)",
      type: "text",
      helpText: "Trigger this workflow for new people",
    },
    {
      key: "matchStrategy",
      label: "Match Strategy",
      type: "select",
      options: [
        { value: "email_first", label: "Email first, then name" },
        { value: "name_first", label: "Name first, then email" },
        { value: "manual", label: "Manual review only" },
      ],
    },
  ],

  setupGuide: {
    summary:
      "Planning Center uses OAuth 2.0 so every church authorizes Echo OCR against their own PCO account. You'll register one OAuth application in your own Planning Center org; churches then click Connect and sign in.",
    docsUrl: "https://developer.planning.center/docs/#/overview/authentication",
    steps: [
      {
        title: "Create a Planning Center account (or use an existing one)",
        body: "You need a Planning Center organization owned by you (not the church's). Planning Center organizations are free to create and are used solely to host the OAuth application that all churches will authorize against.",
        linkUrl: "https://planningcenter.com/signup",
        linkLabel: "Sign up at planningcenter.com",
      },
      {
        title: "Register an OAuth application",
        body: "Sign in to the developer console and create a new application. Only Organization Administrators can create OAuth apps.",
        linkUrl: "https://api.planningcenteronline.com/oauth/applications",
        linkLabel: "Open developer console",
      },
      {
        title: "Configure the redirect URI",
        body: "Set the redirect URI to https://YOUR_DOMAIN/api/integrations/oauth/planning_center/callback — replace YOUR_DOMAIN with the domain where Echo OCR is hosted. For local development, use http://localhost:3000/api/integrations/oauth/planning_center/callback.",
      },
      {
        title: "Choose the scopes",
        body: "At minimum, request the \"people\" scope. If you also want to add matched people to lists or trigger workflows, the people scope already covers those endpoints.",
      },
      {
        title: "Copy the Client ID and Client Secret",
        body: "After creating the application, copy the Application ID and Secret. Add them to Echo OCR's environment as PCO_CLIENT_ID and PCO_CLIENT_SECRET, then restart the server.",
      },
      {
        title: "Connect from the Integrations page",
        body: "Back in Echo OCR, click Connect on the Planning Center card and sign in with your church's PCO account. Echo OCR will store the access + refresh tokens and auto-refresh them every 2 hours.",
      },
      {
        title: "(Optional) Set a default List or Workflow",
        body: "To auto-add matched people to a PCO List, paste the list's numeric ID (from its URL) into \"Default List ID\". The same applies to workflows.",
      },
    ],
  },

  async testConnection(config: JsonValue): Promise<TestResult> {
    try {
      const cfg = parseConfig(config);
      if (!cfg.accessToken) {
        return {
          success: false,
          message:
            "Not connected — use the Connect button to authorize with Planning Center",
        };
      }
      const data = await pcoFetch(cfg, "/people?per_page=1");
      const total = data.meta?.total_count ?? "?";
      return {
        success: true,
        message: `Connected — ${total} people in PCO`,
      };
    } catch (err) {
      return {
        success: false,
        message: err instanceof Error ? err.message : "Connection failed",
      };
    }
  },

  async getExternalFields(config: JsonValue): Promise<ExternalField[]> {
    const cfg = parseConfig(config);
    const builtIn: ExternalField[] = [
      { id: "first_name", name: "First Name", type: "text" },
      { id: "last_name", name: "Last Name", type: "text" },
      { id: "middle_name", name: "Middle Name", type: "text" },
      { id: "nickname", name: "Nickname", type: "text" },
      { id: "gender", name: "Gender", type: "text" },
      { id: "birthdate", name: "Birthdate", type: "date" },
      { id: "anniversary", name: "Anniversary", type: "date" },
      { id: "membership", name: "Membership", type: "text" },
      { id: "status", name: "Status", type: "text" },
      { id: "medical_notes", name: "Medical Notes", type: "text" },
    ];

    try {
      const data = await pcoFetch(cfg, "/field_definitions");
      const rows =
        (data.data as
          | Array<{ id: string; attributes: { name: string; data_type: string } }>
          | undefined) || [];
      const custom = rows.map((fd) => ({
        id: `custom_${fd.id}`,
        name: fd.attributes.name,
        type: fd.attributes.data_type,
      }));
      return [...builtIn, ...custom];
    } catch {
      return builtIn;
    }
  },

  async pushCard(
    card: CardData,
    config: JsonValue,
    mapping: JsonValue
  ): Promise<PushResult> {
    try {
      const cfg = parseConfig(config);
      const fieldMap = (mapping as Record<string, string>) || {};

      if (!cfg.accessToken) {
        return { success: false, message: "Not connected to Planning Center" };
      }

      let personId: string | null = null;
      const { firstName, lastName } = card.name
        ? splitName(card.name)
        : { firstName: "", lastName: "" };

      const strategy = cfg.matchStrategy || "email_first";

      if (strategy === "email_first") {
        if (card.email) {
          personId = await findPersonByEmail(cfg, card.email);
        }
        if (!personId && firstName) {
          personId = await findPersonByName(cfg, firstName, lastName);
        }
      } else if (strategy === "name_first") {
        if (firstName) {
          personId = await findPersonByName(cfg, firstName, lastName);
        }
        if (!personId && card.email) {
          personId = await findPersonByEmail(cfg, card.email);
        }
      }

      // Build person attributes from the field mapping; fall back to defaults
      const personAttrs: Record<string, unknown> = {};
      const customFieldUpdates: Array<{ fieldDefinitionId: string; value: unknown }> = [];

      const hasMapping = fieldMap && Object.keys(fieldMap).length > 0;

      if (hasMapping) {
        for (const [cardField, externalId] of Object.entries(fieldMap)) {
          if (!externalId || cardField.startsWith("_")) continue;
          const val = card[cardField];
          if (val === null || val === undefined || val === "") continue;

          if (externalId.startsWith("custom_")) {
            customFieldUpdates.push({
              fieldDefinitionId: externalId.replace(/^custom_/, ""),
              value: typeof val === "object" ? JSON.stringify(val) : val,
            });
          } else if (BUILT_IN_PERSON_FIELDS.has(externalId)) {
            personAttrs[externalId] = val;
          }
        }
      }

      // Always ensure first/last name are set so that created people aren't blank
      if (!personAttrs.first_name && firstName) personAttrs.first_name = firstName;
      if (!personAttrs.last_name && lastName) personAttrs.last_name = lastName;
      if (!personAttrs.gender && card.gender) personAttrs.gender = card.gender;
      if (!personAttrs.birthdate && card.dateOfBirth)
        personAttrs.birthdate = card.dateOfBirth;

      if (personId) {
        await pcoFetch(cfg, `/people/${personId}`, {
          method: "PATCH",
          body: JSON.stringify({
            data: {
              type: "Person",
              id: personId,
              attributes: personAttrs,
            },
          }),
        });
      } else {
        const createRes = await pcoFetch(cfg, "/people", {
          method: "POST",
          body: JSON.stringify({
            data: {
              type: "Person",
              attributes: personAttrs,
            },
          }),
        });
        personId = ((createRes.data as { id: string }) || {}).id;
      }

      if (!personId) {
        return { success: false, message: "Failed to create or match PCO person" };
      }

      // Write custom field values (if any were mapped)
      for (const update of customFieldUpdates) {
        try {
          await pcoFetch(cfg, `/people/${personId}/field_data`, {
            method: "POST",
            body: JSON.stringify({
              data: {
                type: "FieldDatum",
                attributes: { value: String(update.value) },
                relationships: {
                  field_definition: {
                    data: {
                      type: "FieldDefinition",
                      id: update.fieldDefinitionId,
                    },
                  },
                },
              },
            }),
          });
        } catch (err) {
          console.error("[pco] custom field write failed:", err);
        }
      }

      if (card.email) {
        try {
          await pcoFetch(cfg, `/people/${personId}/emails`, {
            method: "POST",
            body: JSON.stringify({
              data: {
                type: "Email",
                attributes: {
                  address: card.email,
                  location: "Home",
                  primary: true,
                },
              },
            }),
          });
        } catch {
          // email may already exist
        }
      }

      if (card.cellPhone) {
        try {
          await pcoFetch(cfg, `/people/${personId}/phone_numbers`, {
            method: "POST",
            body: JSON.stringify({
              data: {
                type: "PhoneNumber",
                attributes: {
                  number: card.cellPhone,
                  location: "Mobile",
                  primary: true,
                },
              },
            }),
          });
        } catch {
          // phone may already exist
        }
      }

      if (card.homePhone) {
        try {
          await pcoFetch(cfg, `/people/${personId}/phone_numbers`, {
            method: "POST",
            body: JSON.stringify({
              data: {
                type: "PhoneNumber",
                attributes: {
                  number: card.homePhone,
                  location: "Home",
                  primary: !card.cellPhone,
                },
              },
            }),
          });
        } catch {
          // phone may already exist
        }
      }

      // Address sync
      if (card.address || card.city || card.state || card.zip) {
        try {
          const street = [card.address, card.aptNumber]
            .filter(Boolean)
            .join(" ");
          await pcoFetch(cfg, `/people/${personId}/addresses`, {
            method: "POST",
            body: JSON.stringify({
              data: {
                type: "Address",
                attributes: {
                  street: street || "",
                  city: card.city || "",
                  state: card.state || "",
                  zip: card.zip || "",
                  location: "Home",
                  primary: true,
                },
              },
            }),
          });
        } catch {
          // address may already exist
        }
      }

      if (cfg.defaultListId) {
        try {
          await pcoFetch(cfg, `/lists/${cfg.defaultListId}/people`, {
            method: "POST",
            body: JSON.stringify({
              data: { type: "Person", id: personId },
            }),
          });
        } catch {
          // may already be on list
        }
      }

      if (cfg.defaultWorkflowId) {
        try {
          await pcoFetch(cfg, `/workflows/${cfg.defaultWorkflowId}/cards`, {
            method: "POST",
            body: JSON.stringify({
              data: {
                type: "WorkflowCard",
                relationships: {
                  person: { data: { type: "Person", id: personId } },
                },
              },
            }),
          });
        } catch (err) {
          console.error("[pco] workflow enqueue failed:", err);
        }
      }

      return {
        success: true,
        externalId: personId,
        message: `Synced to PCO person ${personId}`,
      };
    } catch (err) {
      return {
        success: false,
        message: err instanceof Error ? err.message : "Push failed",
      };
    }
  },
};
