#!/usr/bin/env python3
"""
find-grep - Search markdown notes by filename and/or content
Usage: find-grep <vault> name:PATTERN content:PATTERN
"""

import argparse
import os
import sys
import re
from pathlib import Path

# ANSI colors
RESET  = "\033[0m"
BOLD   = "\033[1m"
YELLOW = "\033[33m"
CYAN   = "\033[36m"
GREEN  = "\033[32m"
DIM    = "\033[2m"
RED    = "\033[31m"

def highlight(text, pattern, color=YELLOW):
    def replacer(m):
        return f"{color}{BOLD}{m.group(0)}{RESET}"
    return re.sub(re.escape(pattern), replacer, text, flags=re.IGNORECASE)

def parse_filters(terms):
    filters = {"name": None, "content": None}
    for term in terms:
        if ":" in term:
            key, _, value = term.partition(":")
            if key in filters:
                filters[key] = value
            else:
                print(f"{RED}Error: unknown filter '{key}'. Valid filters: name, content{RESET}", file=sys.stderr)
                sys.exit(1)
        else:
            print(f"{RED}Error: '{term}' is not a valid filter. Use name:VALUE or content:VALUE{RESET}", file=sys.stderr)
            sys.exit(1)
    return filters

def search_notes(filters, vault, case_sensitive=False):
    re_flags = 0 if case_sensitive else re.IGNORECASE
    name_pat    = re.compile(re.escape(filters["name"]),    re_flags) if filters["name"]    else None
    content_pat = re.compile(re.escape(filters["content"]), re_flags) if filters["content"] else None

    results = []

    for filepath in sorted(Path(vault).rglob("*.md")):
        name = filepath.stem
        rel_path = filepath.relative_to(vault)

        if name_pat and not name_pat.search(name):
            continue

        content_matches = []
        if content_pat:
            try:
                lines = filepath.read_text(encoding="utf-8").splitlines()
                for i, line in enumerate(lines):
                    if content_pat.search(line):
                        content_matches.append({"line_num": i + 1, "line": line})
            except (UnicodeDecodeError, PermissionError, FileNotFoundError):
                pass
            if not content_matches:
                continue

        results.append({
            "path": filepath,
            "rel_path": rel_path,
            "name": name,
            "name_match": name_pat is not None,
            "content_matches": content_matches,
        })

    return results

def print_results(results, filters, show_context):
    if not results:
        print(f"{DIM}No results found{RESET}")
        return

    total_files = len(results)
    total_hits = sum(len(r["content_matches"]) for r in results)

    for r in results:
        match_tags = []
        if r["name_match"]:
            match_tags.append(f"{GREEN}name:{filters['name']}{RESET}")
        if r["content_matches"]:
            n = len(r["content_matches"])
            match_tags.append(f"{CYAN}content ({n} match{'es' if n != 1 else ''}){RESET}")
        tags = ", ".join(match_tags)

        path_str = str(r["rel_path"])
        if filters["name"]:
            path_str = highlight(path_str, filters["name"], YELLOW)
        print(f"\n{BOLD}{path_str}{RESET}  {DIM}[{tags}]{RESET}")

        if show_context and r["content_matches"]:
            for match in r["content_matches"][:5]:
                line = highlight(match["line"].strip(), filters["content"], YELLOW) if filters["content"] else match["line"].strip()
                print(f"  {DIM}L{match['line_num']}:{RESET} {line}")

    print(f"\n{DIM}─── {total_files} file{'s' if total_files != 1 else ''} matched", end="")
    if total_hits:
        print(f", {total_hits} content hit{'s' if total_hits != 1 else ''}", end="")
    print(f" ───{RESET}")

def main():
    parser = argparse.ArgumentParser(
        prog="find-grep",
        description="Search markdown notes by filename and/or content.",
        usage="find-grep <vault> [name:PATTERN] [content:PATTERN] [-c] [--name-only]",
    )
    parser.add_argument("-c", "--case-sensitive", action="store_true", help="Case-sensitive search")
    parser.add_argument("--no-context", "--name-only", dest="no_context", action="store_true", help="Don't show matching lines, just filenames")

    args, positional = parser.parse_known_args()

    if not positional:
        parser.error("vault is required")

    vault = os.path.expanduser(positional[0])
    if not os.path.isdir(vault):
        print(f"{RED}Error: '{vault}' is not a directory.{RESET}", file=sys.stderr)
        sys.exit(1)

    filter_terms = positional[1:]
    if not filter_terms:
        print(f"{RED}Error: at least one filter required (name:VALUE or content:VALUE){RESET}", file=sys.stderr)
        sys.exit(1)

    filters = parse_filters(filter_terms)

    if not any(filters.values()):
        print(f"{RED}Error: at least one filter required (name:VALUE or content:VALUE){RESET}", file=sys.stderr)
        sys.exit(1)

    results = search_notes(filters, vault, case_sensitive=args.case_sensitive)
    print_results(results, filters, show_context=not args.no_context)


# ---
# I am @readwithai. I make tools for reading and agency with and without obsidian and AI.
# I also make a stream of small tools like this related to my work.
#
# If you are interested in reading or note taking or Obsidian, check out my blog at
# http://readwithai.substack.com/
#
# If you are interested in a stream of tools, you can follow me on GitHub:
# https://github.com/talwrii
# or follow me on X: https://x.com/readwithai

if __name__ == "__main__":
    main()