#!/usr/bin/env python3
"""
AP log collection unit:
- Locate an AP log directory under a root
- Move AP-related logs into `aplog_raw/` and `aplog_filter/` buckets
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional

from fs_utils import copy, copy_preserve, ensure_dir, find_prefixed_ancestor, move, move_preserve
from log_patterns import (
    PATTERNS,
    has_aplog_fragments,
    is_bugreport_txt_name,
    is_ap_text_name,
    is_diag_logs_logs_dir,
    is_filter_dir_name,
    looks_like_aplog_dir,
)
from workflow_layout import SessionLayout


@dataclass(frozen=True)
class AplogLocator:
    def find_aplog_dirs(self, root: Path) -> List[Path]:
        dirs: List[Path] = []
        for dirpath, _, filenames in os.walk(root):
            p = Path(dirpath)
            if any(is_ap_text_name(name) for name in filenames):
                dirs.append(p)
            if looks_like_aplog_dir(p):
                dirs.append(p)
            if has_aplog_fragments(filenames):
                dirs.append(p)
        # unique preserve order
        seen = set()
        out: List[Path] = []
        for d in dirs:
            if d in seen:
                continue
            out.append(d)
            seen.add(d)
        return out

    def find_bugreport_txt_parent(self, root: Path) -> Optional[Path]:
        candidates: List[Path] = []
        for dirpath, _, filenames in os.walk(root):
            for fn in filenames:
                if is_bugreport_txt_name(fn):
                    candidates.append(Path(dirpath) / fn)
        if not candidates:
            return None
        candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True)
        return candidates[0].parent

    def detect_aplog_dir(self, root: Path) -> Optional[Path]:
        candidates = self.find_aplog_dirs(root)
        if not candidates:
            return None

        def score(path: Path) -> tuple[int, int, str]:
            try:
                has_fragments = has_aplog_fragments(os.listdir(path))
            except Exception:
                has_fragments = False
            try:
                depth = len(path.relative_to(root).parts)
            except Exception:
                depth = len(path.parts)
            return (0 if has_fragments else 1, depth, path.as_posix())

        return sorted(candidates, key=score)[0]


@dataclass(frozen=True)
class AplogCollector:
    layout: SessionLayout

    def _collect_extra_ap_dirs(
        self,
        source_root: Path,
        session_root: Path,
        ap_raw_dir: Path,
        preserve_source: bool,
    ) -> None:
        reserved_roots = [session_root] + [session_root / name for name in self.layout.reserved_dirnames()]
        candidates = sorted(
            (p for p in source_root.rglob("*") if p.is_dir()),
            key=lambda p: (len(p.parts), p.as_posix()),
        )
        for p in candidates:
            if any(root == p or root in p.parents for root in reserved_roots):
                continue
            try:
                if not (looks_like_aplog_dir(p) or has_aplog_fragments(os.listdir(p))):
                    continue
            except Exception:
                continue
            if preserve_source:
                copy_preserve(p, source_root, ap_raw_dir)
            else:
                move_preserve(p, source_root, ap_raw_dir)

    def _collect_extra_ap_texts(
        self,
        source_root: Path,
        session_root: Path,
        ap_raw_dir: Path,
        preserve_source: bool,
    ) -> None:
        reserved_roots = [session_root] + [session_root / name for name in self.layout.reserved_dirnames()]
        candidates = sorted(p for p in source_root.rglob("*.txt") if p.is_file())
        for p in candidates:
            if any(root == p or root in p.parents for root in reserved_roots):
                continue
            if not is_ap_text_name(p.name):
                continue
            if preserve_source:
                copy_preserve(p, source_root, ap_raw_dir)
            else:
                move_preserve(p, source_root, ap_raw_dir)

    def _split_ap_raw_and_filter(self, ap_node: Path, ap_filter_dir: Path, others_dir: Path) -> None:
        if not ap_node.exists():
            return

        # Keep hierarchy for "others" relative to this AP node to reduce information loss.
        others_keep = ensure_dir(others_dir / ap_node.name)

        for p in list(ap_node.iterdir()):
            if p.is_dir() and is_filter_dir_name(p.name):
                move(p, ap_filter_dir)
                continue

            if p.is_file():
                # keep AP fragments and *.txt (bugreport/logcat/merged_all) in ap_raw; move others aside
                if p.suffix.lower() != ".txt" and not has_aplog_fragments([p.name]):
                    move_preserve(p, ap_node, others_keep)
                continue

            # directory
            if p.name == PATTERNS.aplog_bugreport_extracted_dirname:
                for q in list(p.iterdir()):
                    if q.is_dir() and is_filter_dir_name(q.name):
                        move(q, ap_filter_dir)
                        continue
                    if q.is_file() and q.suffix.lower() == ".txt" and is_bugreport_txt_name(q.name):
                        continue
                    move_preserve(q, ap_node, others_keep)
                continue

            if p.name == PATTERNS.aplog_logs_dirname:
                for q in list(p.iterdir()):
                    if q.is_file():
                        if has_aplog_fragments([q.name]) or q.suffix.lower() == ".txt":
                            continue
                        move_preserve(q, ap_node, others_keep)
                    else:
                        move_preserve(q, ap_node, others_keep)
                continue

            move_preserve(p, ap_node, others_keep)

    def collect(self, session_root: Path, aplog_dir: Path, source_root: Optional[Path] = None) -> Path:
        """Collect AP logs for this session. Returns the moved AP node under aplog_raw."""
        source_root = (source_root or session_root).resolve()
        ap_raw_dir = ensure_dir(session_root / self.layout.aplog_raw)
        ap_filter_dir = ensure_dir(session_root / self.layout.aplog_filter)
        others_dir = ensure_dir(session_root / self.layout.others)

        ap_node = aplog_dir.parent if is_diag_logs_logs_dir(aplog_dir) else aplog_dir
        ap_node_resolved = ap_node.resolve()
        session_root_resolved = session_root.resolve()
        preserve_source = find_prefixed_ancestor(source_root, "input") is not None

        inside_ap_node = False
        try:
            session_root_resolved.relative_to(ap_node_resolved)
            inside_ap_node = True
        except ValueError:
            inside_ap_node = False

        if preserve_source:
            working_ap_node = ensure_dir(ap_raw_dir / ap_node.name)
            for p in list(ap_node.iterdir()):
                if p.is_file() and (is_ap_text_name(p.name) or has_aplog_fragments([p.name])):
                    copy(p, working_ap_node)
                    continue
                if p.is_dir():
                    try:
                        should_copy = (
                            p.name in PATTERNS.aplog_inplace_dirnames
                            or is_filter_dir_name(p.name)
                            or looks_like_aplog_dir(p)
                            or has_aplog_fragments(os.listdir(p))
                        )
                    except Exception:
                        should_copy = False
                    if should_copy:
                        copy(p, working_ap_node)
            # When source_root is already the detected AP node (for example
            # `input/logcat.txt`), direct child copy above is sufficient.
            # Running the extra collection pass again would duplicate the same
            # AP text into `aplog_raw/` and trigger duplicated `*_filter_1`
            # outputs later.
            if source_root != ap_node_resolved:
                self._collect_extra_ap_dirs(source_root, session_root, ap_raw_dir, preserve_source=True)
                self._collect_extra_ap_texts(source_root, session_root, ap_raw_dir, preserve_source=True)
            self._split_ap_raw_and_filter(working_ap_node, ap_filter_dir, others_dir)
            return working_ap_node

        if ap_node_resolved == session_root_resolved or inside_ap_node:
            # In-place: move well-known children into ap_raw
            for dname in PATTERNS.aplog_inplace_dirnames:
                dpath = ap_node / dname
                if dpath.exists():
                    move(dpath, ap_raw_dir)
            for f in list(ap_node.glob("*.txt")):
                if is_ap_text_name(f.name):
                    move(f, ap_raw_dir)
            working_ap_node = ap_raw_dir
        else:
            working_ap_node = move(ap_node, ap_raw_dir) or (ap_raw_dir / ap_node.name)

        self._collect_extra_ap_dirs(source_root, session_root, ap_raw_dir, preserve_source=False)
        self._collect_extra_ap_texts(source_root, session_root, ap_raw_dir, preserve_source=False)
        self._split_ap_raw_and_filter(working_ap_node, ap_filter_dir, others_dir)
        return working_ap_node
