import sqlite3
from collections.abc import Sized

from lektor.constants import PRIMARY_ALT


def _iter_parents(path):
    path = path.strip("/")
    if path:
        pieces = path.split("/")
        for x in range(len(pieces)):
            yield "/" + "/".join(pieces[:x])


def _find_info(infos, alt, lang):
    for info in infos:
        if info["alt"] == alt and info["lang"] == lang:
            return info
    return None


def _id_from_path(path):
    try:
        return path.strip("/").split("/")[-1]
    except IndexError:
        return ""


def _mapping_from_cursor(cur):
    rv = {}
    for path, alt, lang, type, title in cur.fetchall():
        rv.setdefault(path, []).append(
            {
                "id": _id_from_path(path),
                "path": path,
                "alt": alt,
                "type": type,
                "lang": lang,
                "title": title,
            }
        )
    return rv


def _find_best_info(infos, alt, lang):
    for _alt, _lang in [
        (alt, lang),
        (PRIMARY_ALT, lang),
        (alt, "en"),
        (PRIMARY_ALT, "en"),
    ]:
        rv = _find_info(infos, _alt, _lang)
        if rv is not None:
            return rv
    return None


def _build_parent_path(path, mapping, alt, lang):
    rv = []
    for parent in _iter_parents(path):
        info = _find_best_info(mapping.get(parent) or [], alt, lang)
        id = _id_from_path(parent)
        if info is None:
            title = id or "(Index)"
        else:
            title = info.get("title")
        rv.append({"id": id, "path": parent, "title": title})
    return rv


def _placeholders(values: Sized) -> str:
    """Return SQL '?' placeholders for an array or set of values."""
    return ",".join(["?"] * len(values))


def _process_search_results(builder, cur, alt, lang, limit):
    mapping = _mapping_from_cursor(cur)
    rv = []

    files_needed = set()

    for path, infos in mapping.items():
        info = _find_best_info(infos, alt, lang)
        if info is None:
            continue

        for parent in _iter_parents(path):
            if parent not in mapping:
                files_needed.add(parent)

        rv.append(info)
        if len(rv) == limit:
            break

    if files_needed:
        cur.execute(
            f"""
            select path, alt, lang, type, title
              from source_info
             where path in ({_placeholders(files_needed)})
            """,
            list(files_needed),
        )
        mapping.update(_mapping_from_cursor(cur))

    for info in rv:
        info["parents"] = _build_parent_path(info["path"], mapping, alt, lang)

    return rv


def find_files(builder, query, alt=PRIMARY_ALT, lang=None, limit=50, types=None):
    if types is None:
        types = ["page"]
    else:
        types = list(types)
    languages = ["en"]
    if lang not in ("en", None):
        languages.append(lang)
    else:
        lang = "en"
    alts = [PRIMARY_ALT]
    if alt != PRIMARY_ALT:
        alts.append(alt)

    query = query.strip()
    title_like = "%" + query + "%"
    path_like = "/%" + query.rstrip("/") + "%"

    con = sqlite3.connect(builder.buildstate_database_filename, timeout=10)
    try:
        cur = con.cursor()
        cur.execute(
            f"""
            select path, alt, lang, type, title
              from source_info
             where (title like ? or path like ?)
               and lang in ({_placeholders(languages)})
               and alt in ({_placeholders(alts)})
               and type in ({_placeholders(types)})
          order by title
           collate nocase
             limit ?
            """,
            [title_like, path_like] + languages + alts + types + [limit * 2],
        )
        return _process_search_results(builder, cur, alt, lang, limit)
    finally:
        con.close()
