"""
Project list: get_projects_list, filter_forks, search_projects_list, project_in_list,
get_project_owner, get_project_list_from_file, get_last_activity.
Ported from gitweb/gitweb.perl.
"""
from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Any, Callable

from pygitweb.config import PROJECTROOT, PROJECTS_LIST, PROJECT_MAXDEPTH, LIST_ALL
from pygitweb.validation import check_export_ok


def _export_ok_path(git_dir: str, export_ok: str) -> bool:
    return not export_ok or os.path.isfile(os.path.join(git_dir, export_ok))


def project_in_list(
    project: str,
    get_projects_list_fn: Callable[[], list[dict[str, Any]]],
) -> bool:
    """True if project appears in project list. Port of project_in_list."""
    lst = get_projects_list_fn()
    return any(p.get("path") == project for p in lst)


def _find_projects_in_dir(
    root: str,
    prefix_len: int,
    prefix_depth: int,
    maxdepth: int,
    export_ok: str,
    export_auth_hook: Callable[[str], bool] | None,
    skip_export_check: bool = False,
) -> list[dict[str, Any]]:
    result = []
    for dirpath, dirnames, _ in os.walk(root, topdown=True):
        rel = os.path.relpath(dirpath, root)
        if rel == ".":
            depth = 0
        else:
            depth = rel.count(os.sep) + 1
        if depth > maxdepth:
            dirnames.clear()
            continue
        for d in list(dirnames):
            path = os.path.join(dirpath, d)
            if not os.path.isdir(path):
                continue
            try:
                if not os.access(path, os.X_OK):
                    continue
            except OSError:
                continue
            project_path = os.path.relpath(path, PROJECTROOT)
            project_path = project_path.replace("\\", "/")
            git_dir = os.path.join(PROJECTROOT, project_path)
            if not skip_export_check and not check_export_ok(git_dir, export_ok, export_auth_hook):
                continue
            result.append({"path": project_path})
            dirnames.remove(d)
    return result


def git_get_projects_list(
    filter_path: str = "",
    paranoid: bool = False,
    projectroot: str = PROJECTROOT,
    projects_list: str = PROJECTS_LIST,
    project_maxdepth: int = PROJECT_MAXDEPTH,
    export_ok: str = "",
    export_auth_hook: Callable[[str], bool] | None = None,
) -> list[dict[str, Any]]:
    """List projects from directory scan or file. Port of git_get_projects_list."""
    if os.path.isdir(projects_list):
        root = projects_list.rstrip("/")
        prefix_len = len(root) + 1
        prefix_depth = root.count(os.sep)
        if filter_path and not paranoid:
            root = os.path.join(root, filter_path).rstrip("/")
        result = _find_projects_in_dir(
            root, prefix_len, prefix_depth, project_maxdepth,
            export_ok, export_auth_hook,
            skip_export_check=LIST_ALL,
        )
        if filter_path and paranoid:
            result = [p for p in result if p["path"].startswith(filter_path + "/")]
        return result
    if os.path.isfile(projects_list):
        from urllib.parse import unquote
        result = []
        with open(projects_list) as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                parts = line.split(None, 1)
                path = unquote(parts[0]) if parts else ""
                owner = unquote(parts[1]) if len(parts) > 1 else None
                if not path:
                    continue
                if filter_path and not path.startswith(filter_path + "/"):
                    continue
                git_dir = os.path.join(projectroot, path)
                if not LIST_ALL and not check_export_ok(git_dir, export_ok, export_auth_hook):
                    continue
                pr = {"path": path}
                if owner:
                    pr["owner"] = owner
                result.append(pr)
        return result
    return []


_gitweb_project_owner: dict[str, str] | None = None


def git_get_project_list_from_file(
    projects_list: str = PROJECTS_LIST,
    projectroot: str = PROJECTROOT,
) -> dict[str, str]:
    """Load project -> owner from file. Port of git_get_project_list_from_file."""
    global _gitweb_project_owner
    if _gitweb_project_owner is not None:
        return _gitweb_project_owner
    _gitweb_project_owner = {}
    if os.path.isfile(projects_list):
        from urllib.parse import unquote
        with open(projects_list) as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                parts = line.split(None, 1)
                path = unquote(parts[0]) if parts else ""
                owner = unquote(parts[1]) if len(parts) > 1 else ""
                if path:
                    _gitweb_project_owner[path] = owner
    return _gitweb_project_owner


def git_get_project_owner(
    project: str,
    projectroot: str = PROJECTROOT,
    get_project_config: Callable[[str, str], Any] | None = None,
) -> str | None:
    """Owner from list file or config or file ownership. Port of git_get_project_owner."""
    if not project:
        return None
    owners = git_get_project_list_from_file(PROJECTS_LIST, projectroot)
    if project in owners and owners[project]:
        return owners[project]
    if get_project_config:
        val = get_project_config(project, "owner")
        if val:
            return val[0] if isinstance(val, list) else val
    git_dir = os.path.join(projectroot, project)
    try:
        stat = os.stat(git_dir)
        import pwd
        return pwd.getpwuid(stat.st_uid).pw_gecos or pwd.getpwuid(stat.st_uid).pw_name
    except (OSError, KeyError):
        return None


def git_get_last_activity(project: str, projectroot: str = PROJECTROOT) -> int | None:
    """Last commit timestamp for project. Port of git_get_last_activity."""
    from pygitweb.git_helpers import git_get_head_hash, parse_commit

    oid = git_get_head_hash(project)
    if not oid:
        return None
    co = parse_commit(project, oid)
    return co.get("committer_epoch")


def filter_forks_from_projects_list(
    projects: list[dict[str, Any]],
    projectroot: str = PROJECTROOT,
) -> list[dict[str, Any]]:
    """Remove forks from list; set 'forks' on each project. Port of filter_forks_from_projects_list."""
    trie: dict[str, Any] = {}
    for pr in projects:
        path = pr.get("path", "")
        path_no_git = path.replace(".git", "")
        if path_no_git.endswith("/") or not path_no_git:
            pr["forks"] = []
            continue
        if not os.path.isdir(os.path.join(projectroot, path)):
            pr["forks"] = []
            continue
        pr["forks"] = []
        dirs = path_no_git.split("/")
        ref = trie
        for d in dirs:
            ref = ref.setdefault(d, {})
        ref[""] = pr

    filtered = []
    for pr in projects:
        path = pr.get("path", "")
        dirs = path.split("/")
        ref = trie
        for d in dirs:
            if "" in ref:
                ref[""].setdefault("forks", []).append(pr)
                break
            if d not in ref:
                filtered.append(pr)
                break
            ref = ref[d]
        else:
            if "" not in ref:
                filtered.append(pr)
    return filtered


def search_projects_list(
    projlist: list[dict[str, Any]],
    tagfilter: str | None = None,
    search_regexp: str | None = None,
    fill_project_list_info: Callable[..., None] | None = None,
) -> list[dict[str, Any]]:
    """Filter by tag or search regex. Port of search_projects_list."""
    if not tagfilter and not search_regexp:
        return projlist
    if fill_project_list_info:
        fill_project_list_info(projlist, tagfilter=tagfilter, search_re=search_regexp)
    result = []
    for pr in projlist:
        if tagfilter:
            ctags = pr.get("ctags") or {}
            if not any(k.lower() == tagfilter.lower() for k in ctags):
                continue
        if search_regexp:
            try:
                rex = re.compile(search_regexp)
            except re.error:
                continue
            descr = (pr.get("descr_long") or "") + (pr.get("path") or "")
            if not rex.search(descr):
                continue
        result.append(pr)
    return result