#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
"""
manifest_collectors.py - Document and contact collection for manifest generation.

Extracted from generate-manifest.py to reduce file-level complexity.
"""

import os
import glob
from collections import OrderedDict
from typing import Any, Dict, List


def _strip_quotes(value: str) -> str:
    """Strip surrounding single or double quotes from a YAML value."""
    if len(value) >= 2 and value[0] in ('"', "'") and value[-1] == value[0]:
        return value[1:-1]
    return value


def _parse_fm_line(line: str, fields: Dict[str, str]) -> None:
    """Parse a single frontmatter key: value line into fields."""
    colon_pos = line.find(':')
    if colon_pos > 0:
        key = line[:colon_pos].strip()
        value = _strip_quotes(line[colon_pos + 1:].strip())
        fields[key] = value


def _read_frontmatter_block(md_path: str) -> str:
    """Read and return the raw frontmatter block from a markdown file, or ''."""
    try:
        with open(md_path, 'r', encoding='utf-8', errors='replace') as f:
            content = f.read(8192)
    except (OSError, IOError):
        return ''
    if not content.startswith('---'):
        return ''
    end = content.find('\\n---', 3)
    if end == -1:
        return ''
    return content[4:end]


def parse_frontmatter(md_path: str) -> Dict[str, str]:
    """Extract YAML frontmatter fields from a markdown file."""
    fields: Dict[str, str] = {}
    fm_block = _read_frontmatter_block(md_path)
    if not fm_block:
        return fields

    for raw_line in fm_block.split('\\n'):
        if raw_line.startswith((' ', '\\t')):
            continue
        line = raw_line.strip()
        if not line or line.startswith('#') or line.startswith('- '):
            continue
        _parse_fm_line(line, fields)

    return fields


def escape_toon_value(val: Any) -> str:
    """Escape a value for TOON format — quote if it contains commas or quotes."""
    val = str(val)
    if ',' in val or '"' in val or '\\n' in val:
        return '"' + val.replace('"', '""') + '"'
    return val


def parse_contact_toon(toon_path: str) -> Dict[str, str]:
    """Parse a contact .toon file into a dict."""
    contact: Dict[str, str] = {}
    try:
        with open(toon_path, 'r', encoding='utf-8', errors='replace') as f:
            for line in f:
                line = line.strip()
                if line == 'contact' or not line:
                    continue
                parts = line.split('\\t', 1)
                if len(parts) == 2:
                    contact[parts[0]] = parts[1]
    except (OSError, IOError):
        pass
    return contact


def find_thread_root(mid: str, reply_chains: Dict[str, str]) -> str:
    """Walk in_reply_to chain to find the root message_id."""
    visited = set()
    current = mid
    while current in reply_chains and current not in visited:
        visited.add(current)
        current = reply_chains[current]
    return current


def collect_documents(output_dir: str) -> tuple:
    """Collect and index documents from markdown files."""
    md_files = sorted(glob.glob(os.path.join(output_dir, '*.md')))
    documents: List[OrderedDict] = []
    msg_id_map: Dict[str, int] = {}
    thread_map: Dict[str, List[int]] = {}
    reply_chains: Dict[str, str] = {}

    for md_path in md_files:
        basename = os.path.basename(md_path)
        if basename.startswith('_'):
            continue

        fm = parse_frontmatter(md_path)
        if not fm:
            continue

        doc: OrderedDict = OrderedDict()
        doc['file'] = basename
        doc['subject'] = fm.get('subject', fm.get('title', ''))
        doc['from'] = fm.get('from', '')
        doc['to'] = fm.get('to', '')
        doc['date_sent'] = fm.get('date_sent', '')
        doc['message_id'] = fm.get('message_id', '')
        doc['in_reply_to'] = fm.get('in_reply_to', '')
        doc['attachment_count'] = fm.get('attachment_count', '0')
        doc['tokens_estimate'] = fm.get('tokens_estimate', '0')
        doc['size'] = fm.get('size', '')
        doc['thread_id'] = fm.get('thread_id', '')
        doc['thread_position'] = fm.get('thread_position', '')

        idx = len(documents)
        documents.append(doc)

        mid = doc['message_id']
        if mid:
            msg_id_map[mid] = idx

        irt = doc['in_reply_to']
        if irt and mid:
            reply_chains[mid] = irt

        tid = doc['thread_id']
        if tid:
            thread_map.setdefault(tid, []).append(idx)

    return documents, msg_id_map, thread_map, reply_chains


def collect_contacts(
    output_dir: str, documents: List[OrderedDict]
) -> List[OrderedDict]:
    """Collect contacts from .toon files and count email interactions."""
    contacts_dir = os.path.join(output_dir, 'contacts')
    contacts: List[OrderedDict] = []

    if not os.path.isdir(contacts_dir):
        return contacts

    toon_files = sorted(glob.glob(os.path.join(contacts_dir, '*.toon')))
    for toon_path in toon_files:
        c = parse_contact_toon(toon_path)
        if not c.get('email'):
            continue

        email_addr = c['email'].lower()
        email_count = sum(
            1 for doc in documents
            if email_addr in doc.get('from', '').lower()
            or email_addr in doc.get('to', '').lower()
        )

        contact: OrderedDict = OrderedDict()
        contact['email'] = c.get('email', '')
        contact['name'] = c.get('name', '')
        contact['title'] = c.get('title', '')
        contact['company'] = c.get('company', '')
        contact['email_count'] = str(email_count)
        contact['first_seen'] = c.get('first_seen', '')
        contact['last_seen'] = c.get('last_seen', '')
        contact['confidence'] = c.get('confidence', 'low')
        contacts.append(contact)

    return contacts
