# AccountPilot — unified per-machine account sync framework
# Copyright (C) 2026 Hasan Arda Eren <ardaeren13@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""Identity normalization, find-or-create, and merge logic."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING

import phonenumbers

if TYPE_CHECKING:
    import aiosqlite


def normalize_email(raw: str) -> str:
    """Lowercase, strip whitespace, drop a `mailto:` prefix."""
    s = raw.strip()
    if s.lower().startswith("mailto:"):
        s = s[len("mailto:") :]
    return s.strip().lower()


def normalize_phone(raw: str, *, default_region: str | None = None) -> str:
    """Best-effort E.164 normalization. Returns stripped raw if unparseable."""
    s = raw.strip()
    try:
        parsed = phonenumbers.parse(s, default_region)
    except phonenumbers.NumberParseException:
        return s
    if not phonenumbers.is_possible_number(parsed):
        return s
    return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)


def normalize_handle(raw: str) -> str:
    """Dispatch by shape.

    Phone-like → E.164, email-like → lowercase, else lowercase strip.
    """
    s = raw.strip()
    if "@" in s:
        return normalize_email(s)
    if s.startswith("+") or s.replace(" ", "").replace("-", "").isdigit():
        normalized = normalize_phone(s)
        if normalized != s:
            return normalized
    return s.lower()


def kind_for_imessage_handle(raw: str) -> str:
    """Dispatch an iMessage handle to the right `identifiers.kind`.

    Cross-source identity (acceptance AP-SP2 §7.3 #2): a phone-shaped
    iMessage handle should collide with phones already stored from a
    Gmail correspondent so they resolve to the same `people` row. Same
    for email-shaped handles. Anything that doesn't match a known shape
    falls back to 'imessage_handle' (an Apple Account / Game Center
    handle, for example).
    """
    s = raw.strip()
    if "@" in s:
        return "email"
    if s.startswith("+"):
        normalized = normalize_phone(s)
        if normalized.startswith("+"):
            return "phone"
    return "imessage_handle"


async def find_or_create_person(
    db: aiosqlite.Connection,
    *,
    kind: str,
    value: str,
    default_name: str | None = None,
) -> int:
    """Look up the identifier; return person_id, creating both rows if absent."""
    if kind == "email":
        normalized = normalize_email(value)
    elif kind == "phone":
        normalized = normalize_phone(value)
    else:
        normalized = normalize_handle(value)

    async with db.execute(
        "SELECT person_id FROM identifiers WHERE kind=? AND value=?",
        (kind, normalized),
    ) as cur:
        row = await cur.fetchone()
    if row is not None:
        return int(row["person_id"])

    name, surname = _split_display_name(default_name)
    now = datetime.now(UTC).isoformat()
    cur2 = await db.execute(
        "INSERT INTO people (name, surname, is_owner, created_at, updated_at) "
        "VALUES (?, ?, 0, ?, ?)",
        (name, surname, now, now),
    )
    person_id = cur2.lastrowid
    assert person_id is not None
    await db.execute(
        "INSERT INTO identifiers (person_id, kind, value, is_primary, created_at) "
        "VALUES (?, ?, ?, 0, ?)",
        (person_id, kind, normalized, now),
    )
    await db.commit()
    return person_id


def _split_display_name(name: str | None) -> tuple[str, str | None]:
    """Split 'Foo Bar' → ('Foo', 'Bar'); single token → ('Foo', None); missing
    → ('Unknown', None)."""
    if not name or not name.strip():
        return "Unknown", None
    parts = name.strip().split(maxsplit=1)
    if len(parts) == 1:
        return parts[0], None
    return parts[0], parts[1]


async def merge_people(
    db: aiosqlite.Connection, *, keep_id: int, discard_id: int
) -> None:
    """Re-point all FKs from `discard_id` to `keep_id`, then delete discarded.

    Single transaction. Self-merge raises ValueError.
    """
    if keep_id == discard_id:
        raise ValueError("cannot merge a person with themselves")

    await db.execute("BEGIN")
    try:
        # Drop discarded identifiers whose (kind, value) already exist on keep
        # (UNIQUE collision avoidance — schema enforces UNIQUE on (kind, value)).
        await db.execute(
            "DELETE FROM identifiers WHERE person_id=? AND (kind, value) IN ("
            "  SELECT kind, value FROM identifiers WHERE person_id=?"
            ")",
            (discard_id, keep_id),
        )
        # Repoint the rest.
        await db.execute(
            "UPDATE identifiers SET person_id=? WHERE person_id=?",
            (keep_id, discard_id),
        )
        await db.execute(
            "UPDATE accounts SET owner_id=? WHERE owner_id=?",
            (keep_id, discard_id),
        )
        # message_people PK is (message_id, person_id, role); duplicates after
        # repointing are silently skipped via INSERT OR IGNORE.
        await db.execute(
            "INSERT OR IGNORE INTO message_people (message_id, person_id, role) "
            "SELECT message_id, ?, role FROM message_people WHERE person_id=?",
            (keep_id, discard_id),
        )
        await db.execute("DELETE FROM message_people WHERE person_id=?", (discard_id,))
        await db.execute("DELETE FROM people WHERE id=?", (discard_id,))
        await db.execute("COMMIT")
    except Exception:
        await db.execute("ROLLBACK")
        raise
