import { AppState, DeviceEventEmitter, Linking, Platform } from 'react-native';
import {
  consumePendingCall,
  isCallWatchJournalLocked,
  lockCallWatchForPhone,
  peekJournalReturnTo,
} from './callTracker';
import {
  clearIncomingUiSuppress,
  getCallUiActive,
  isCallUiBusy,
  isIncomingUiSuppressed,
  openCallUi,
} from './callUiNav';
import {
  getActivityCallDeepLink,
  getPendingIncomingCall,
  requestCallWatchPermissions,
} from './callPermissions';
import {
  isCallDetectActive,
  loadCallDetectSettings,
} from './callDetectSettings';
import { fetchDeviceCallLog, isDeviceCallLogSupported } from './deviceCallLog';
import { normalizePhoneInput } from './phone';

const CALL_DEEP_LINK_EVENT = 'ImmotechCallDeepLink';

export {
  checkCallWatchPermissions,
  openAppPermissionSettings,
  openBatteryOptimizationSettings,
  openFullScreenIntentSettings,
  openOverlayPermissionSettings,
  refreshCallAuthorizations,
  requestCallWatchPermissions,
} from './callPermissions';

const OPEN_COOLDOWN_MS = 90_000;
const RECENT_CALL_MAX_AGE_MS = 90_000;
const MIN_AWAY_MS = 4_000;
/** Ignore call-log / AppState reopen right after a deep link already opened the UI. */
const DEEP_LINK_GRACE_MS = 25_000;
const URL_DEDUPE_MS = 8_000;

/** Dedupe by call identity. */
const openedCallIds = new Set<string>();
let watching = false;
let leftAppAt = 0;
/** Last time we opened any call UI from a deep link / watch path. */
let lastCallUiOpenedAt = 0;
let lastCallUiPhone = '';
/** Last journal open — hard stop for a second “type call info” modal. */
let lastJournalOpenedAt = 0;
let lastJournalPhone = '';
let lastUrlHandled = '';
let lastUrlAt = 0;

function markOpened(callId: string): boolean {
  if (openedCallIds.has(callId)) return false;
  openedCallIds.add(callId);
  if (openedCallIds.size > 40) {
    const first = openedCallIds.values().next().value;
    if (first) openedCallIds.delete(first);
  }
  return true;
}

function noteCallUiOpened(phone: string, kind: 'incoming' | 'post'): void {
  lastCallUiOpenedAt = Date.now();
  lastCallUiPhone = phone ? normalizePhoneInput(phone) : '';
  if (kind === 'post') {
    lastJournalOpenedAt = Date.now();
    lastJournalPhone = lastCallUiPhone;
  }
}

/** Call when journal screen mounts (manual or auto) so AppState never opens a 2nd form. */
export function markJournalSessionOpened(phone?: string): void {
  noteCallUiOpened(phone ?? '', 'post');
  if (phone) lockCallWatchForPhone(phone);
}

function recentlyOpenedCallUi(phone?: string): boolean {
  if (Date.now() - lastCallUiOpenedAt > DEEP_LINK_GRACE_MS) return false;
  if (!phone) return true;
  const n = normalizePhoneInput(phone);
  if (!n || !lastCallUiPhone) return true;
  return (
    n === lastCallUiPhone ||
    n.endsWith(lastCallUiPhone.slice(-8)) ||
    lastCallUiPhone.endsWith(n.slice(-8))
  );
}

/** True if a journal for this (or any) call was just opened — never open a second one. */
function recentlyOpenedJournal(phone?: string): boolean {
  if (Date.now() - lastJournalOpenedAt > DEEP_LINK_GRACE_MS) return false;
  if (!phone) return true;
  const n = normalizePhoneInput(phone);
  if (!n || !lastJournalPhone) return true;
  return (
    n === lastJournalPhone ||
    n.endsWith(lastJournalPhone.slice(-8)) ||
    lastJournalPhone.endsWith(n.slice(-8))
  );
}

function phoneSessionId(kind: 'in' | 'post', phone: string): string {
  const bucket = Math.floor(Date.now() / OPEN_COOLDOWN_MS);
  return `${kind}:${phone}:${bucket}`;
}

function navigatePostCall(opts: {
  phone?: string;
  callType?: 'Sortant' | 'Entrant' | 'Rappel';
  /** emis | recu | absence — display hint from device call log / hangup. */
  callOutcome?: 'emis' | 'recu' | 'absence';
  clientId?: number;
  clientName?: string;
  callId?: string;
}): void {
  if (!isCallDetectActive()) return;
  const phone = opts.phone ? normalizePhoneInput(opts.phone) : '';
  const callType = opts.callType ?? 'Sortant';
  const ui = getCallUiActive();

  // Already on journal → keep the same screen (no second modal).
  if (ui === 'post') return;
  if (recentlyOpenedJournal(phone || undefined)) return;

  // Hangup while incoming overlay is open → swap to journal (always allowed once).
  const fromIncoming = ui === 'incoming';
  if (!fromIncoming && phone && isCallWatchJournalLocked(phone) && recentlyOpenedCallUi(phone)) {
    return;
  }
  if (!fromIncoming && isCallUiBusy() && ui !== 'incoming') return;

  const callId =
    (phone ? phoneSessionId('post', phone) : null) ??
    opts.callId ??
    `${Date.now()}:${phone}|${callType}`;

  if (!fromIncoming && !markOpened(callId)) return;
  if (fromIncoming) markOpened(callId);
  if (phone) lockCallWatchForPhone(phone);
  noteCallUiOpened(phone, 'post');

  const returnTo = peekJournalReturnTo();
  const params: Record<string, string> = {
    ...(phone ? { phone } : {}),
    callType,
    mode: 'journal',
    auto: '1',
    ...(opts.callOutcome ? { callOutcome: opts.callOutcome } : {}),
    ...(opts.clientId ? { clientId: String(opts.clientId) } : {}),
    ...(opts.clientName ? { clientName: opts.clientName } : {}),
    ...(returnTo ? { returnTo } : {}),
  };
  openCallUi('post', params);
}

function navigateIncomingCall(opts: { phone?: string; callId?: string }): void {
  if (!isCallDetectActive()) return;
  const phone = opts.phone ? normalizePhoneInput(opts.phone) : '';
  const ui = getCallUiActive();

  // User tapped Fermer — never re-open « Numéro masqué » / empty peeks for this call.
  if (isIncomingUiSuppressed() && !phone) return;

  // Never cover an open journal — but do NOT block peek when phone is still unknown.
  if (ui === 'post') return;
  if (phone && recentlyOpenedJournal(phone)) return;

  const callId =
    opts.callId ??
    (phone
      ? phoneSessionId('in', phone)
      : `incoming:unknown:${Math.floor(Date.now() / 120_000)}`);

  // Already showing incoming → refresh only when a real number arrives (not empty spam).
  if (ui === 'incoming') {
    if (!phone) return;
    if (phone) lockCallWatchForPhone(phone, 180_000);
    noteCallUiOpened(phone, 'incoming');
    openCallUi('incoming', {
      phone,
      auto: '1',
    });
    return;
  }

  if (!markOpened(callId)) return;
  if (phone) lockCallWatchForPhone(phone, 180_000);
  noteCallUiOpened(phone, 'incoming');

  openCallUi('incoming', {
    ...(phone ? { phone } : {}),
    auto: '1',
  });
}

function parseCallOutcome(raw: string | null | undefined): 'emis' | 'recu' | 'absence' | undefined {
  if (raw === 'emis' || raw === 'recu' || raw === 'absence') return raw;
  return undefined;
}

function parseCallDeepLink(
  url: string | null,
):
  | { kind: 'incoming'; phone?: string; callId?: string }
  | {
      kind: 'post';
      phone?: string;
      callType?: 'Sortant' | 'Entrant';
      callOutcome?: 'emis' | 'recu' | 'absence';
      callId?: string;
    }
  | null {
  if (!url) return null;
  const isIncoming = url.includes('incoming-call');
  const isPost = url.includes('post-call');
  if (!isIncoming && !isPost) return null;

  try {
    const normalized = url.replace(/^immotechcall:\/\//, 'https://immotech.call/');
    const u = new URL(normalized);
    const phone = u.searchParams.get('phone') ?? undefined;
    if (isIncoming) {
      return {
        kind: 'incoming',
        phone,
        callId: phone
          ? `url-in:${phone}:${Math.floor(Date.now() / OPEN_COOLDOWN_MS)}`
          : `url-in:${Math.floor(Date.now() / 8_000)}`,
      };
    }
    const rawType = u.searchParams.get('callType') ?? '';
    const callType =
      rawType === 'Entrant' || rawType === 'Sortant' || rawType === 'Rappel'
        ? (rawType as 'Sortant' | 'Entrant')
        : undefined;
    return {
      kind: 'post',
      phone,
      callType,
      callOutcome: parseCallOutcome(u.searchParams.get('callOutcome')),
      callId: phone
        ? `url:${phone}:${Math.floor(Date.now() / OPEN_COOLDOWN_MS)}`
        : undefined,
    };
  } catch {
    const phoneMatch = /[?&]phone=([^&]+)/.exec(url);
    const phone = phoneMatch ? decodeURIComponent(phoneMatch[1]) : undefined;
    if (isIncoming) return { kind: 'incoming', phone };
    const typeMatch = /[?&]callType=([^&]+)/.exec(url);
    const outcomeMatch = /[?&]callOutcome=([^&]+)/.exec(url);
    return {
      kind: 'post',
      phone,
      callType: typeMatch?.[1] === 'Entrant' ? 'Entrant' : 'Sortant',
      callOutcome: parseCallOutcome(outcomeMatch?.[1]),
    };
  }
}

/** On return to app: open journal for the single latest recent call only. */
async function openFromRecentCallLog(): Promise<void> {
  if (!isDeviceCallLogSupported()) return;
  if (isCallUiBusy()) return;
  if (recentlyOpenedCallUi()) return;
  if (recentlyOpenedJournal()) return;

  const { status, calls } = await fetchDeviceCallLog(8);
  if (status !== 'ok' || !calls.length) return;

  const recent = calls.filter((c) => {
    const age = Date.now() - c.timestamp;
    return age >= 0 && age <= RECENT_CALL_MAX_AGE_MS;
  });
  if (!recent.length) return;

  const latest = recent[0];
  const freshOutgoing = recent.find(
    (c) => c.type === 'outgoing' && Date.now() - c.timestamp <= 45_000,
  );
  const pick =
    latest.type !== 'outgoing' && freshOutgoing && latest.timestamp - freshOutgoing.timestamp < 15_000
      ? freshOutgoing
      : latest;

  const callType: 'Sortant' | 'Entrant' =
    pick.type === 'outgoing' ? 'Sortant' : 'Entrant';
  const callOutcome: 'emis' | 'recu' | 'absence' =
    pick.type === 'outgoing' ? 'emis' : pick.type === 'missed' ? 'absence' : 'recu';
  const phone = pick.phoneNormalized || pick.phone;
  if (!phone) return;
  if (recentlyOpenedCallUi(phone) || recentlyOpenedJournal(phone)) return;

  navigatePostCall({
    phone,
    callType,
    callOutcome,
    clientName: pick.name ?? undefined,
    callId: `log:${pick.timestamp}:${phone}`,
  });
}

/**
 * Samsung often blocks background startActivity during RINGING — only a heads-up
 * shows. When the user opens the app (or taps the notification), recover the CRM modal.
 * Never treat a stuck “busy” lock as final — incoming can refresh / retry.
 */
async function openPendingIncomingIfAny(): Promise<boolean> {
  if (!isCallDetectActive()) return false;
  const ui = getCallUiActive();
  if (ui === 'post') return false;

  const activityUrl = await getActivityCallDeepLink();
  if (activityUrl) {
    const parsed = parseCallDeepLink(activityUrl);
    if (parsed?.kind === 'incoming') {
      if (isIncomingUiSuppressed() && !parsed.phone?.trim()) return false;
      if (parsed.phone?.trim()) clearIncomingUiSuppress();
      navigateIncomingCall({ phone: parsed.phone, callId: parsed.callId });
      return true;
    }
    if (parsed?.kind === 'post') {
      navigatePostCall({
        phone: parsed.phone,
        callType: parsed.callType,
        callOutcome: parsed.callOutcome,
        callId: parsed.callId,
      });
      return true;
    }
  }

  // Native returns null after Fermer until the next RINGING — that stops the 2s poll.
  const pending = await getPendingIncomingCall();
  if (!pending?.inCall) return false;
  if (recentlyOpenedJournal(pending.phone || undefined)) return false;

  const phone = pending.phone?.trim() || undefined;
  if (isIncomingUiSuppressed() && !phone) return false;
  // New RINGING cleared native dismiss → lift JS suppress for this call.
  if (phone) clearIncomingUiSuppress();

  // Stable id for the whole call (not a rotating 8s bucket that bypasses dedupe).
  const sessionAt = pending.at && pending.at > 0 ? Math.floor(pending.at) : 0;
  navigateIncomingCall({
    phone,
    callId: phone
      ? `pending-in:${phone}:${sessionAt || phoneSessionId('in', phone)}`
      : `pending-in:unknown:${sessionAt || 'session'}`,
  });
  return true;
}

/**
 * Deep links from Android CallEndReceiver + AppState fallbacks.
 * RINGING → peek / notification; OFFHOOK → retry; IDLE → one journal only.
 * If the OS only showed a background banner, opening the app still opens the modal.
 */
export function startCallWatch(): () => void {
  if (watching) return () => {};
  watching = true;

  // Load detect toggles (phone / WhatsApp) before any recovery poll.
  void loadCallDetectSettings();
  // Best-effort prompts; never block call recovery (timeouts inside).
  void requestCallWatchPermissions();

  const onUrl = (url: string | null) => {
    if (!url) return;
    // getInitialURL + url event often fire twice on Samsung.
    if (url === lastUrlHandled && Date.now() - lastUrlAt < URL_DEDUPE_MS) return;
    lastUrlHandled = url;
    lastUrlAt = Date.now();

    const parsed = parseCallDeepLink(url);
    if (!parsed) return;
    if (parsed.kind === 'incoming') {
      if (isIncomingUiSuppressed() && !parsed.phone?.trim()) return;
      navigateIncomingCall({ phone: parsed.phone, callId: parsed.callId });
      return;
    }
    navigatePostCall({
      phone: parsed.phone,
      callType: parsed.callType,
      callOutcome: parsed.callOutcome,
      callId: parsed.callId,
    });
  };

  void Linking.getInitialURL().then(onUrl);
  const linkSub = Linking.addEventListener('url', ({ url }) => onUrl(url));

  // MainActivity emits on RCTDeviceEventEmitter when Linking misses onNewIntent (Samsung).
  const nativeSub =
    Platform.OS === 'android'
      ? DeviceEventEmitter.addListener(CALL_DEEP_LINK_EVENT, (url: string) => onUrl(url))
      : undefined;

  // Cold start / resume: recover modal if native launch was blocked.
  void openPendingIncomingIfAny();

  // While a call is in progress, keep trying (Samsung often delivers prefs before Linking).
  const pollId = setInterval(() => {
    if (AppState.currentState !== 'active') return;
    if (getCallUiActive() === 'incoming' || getCallUiActive() === 'post') return;
    void openPendingIncomingIfAny();
  }, 2_000);

  const appSub = AppState.addEventListener('change', (state) => {
    if (state === 'background' || state === 'inactive') {
      leftAppAt = Date.now();
      return;
    }
    if (state !== 'active') return;

    void (async () => {
      // Prefer CRM peek while a call is still ringing / in progress.
      if (await openPendingIncomingIfAny()) return;

      if (isCallUiBusy()) return;
      if (recentlyOpenedCallUi()) return;
      if (recentlyOpenedJournal()) return;

      const pending = consumePendingCall(MIN_AWAY_MS);
      if (pending) {
        navigatePostCall({
          phone: pending.phone,
          callType: 'Sortant',
          clientId: pending.clientId,
          clientName: pending.clientName,
          callId: `pending:${pending.startedAt}:${pending.phone}`,
        });
        return;
      }

      const awayMs = leftAppAt ? Date.now() - leftAppAt : 0;
      if (awayMs >= MIN_AWAY_MS) {
        setTimeout(() => {
          void openFromRecentCallLog();
        }, 900);
      }
    })();
  });

  return () => {
    watching = false;
    linkSub.remove();
    nativeSub?.remove();
    appSub.remove();
    clearInterval(pollId);
  };
}
