"""Microarray adapter for consumer genetics data.

Parses raw genotype files from 23andMe, AncestryDNA, and similar
consumer genotyping services. Extracts mtDNA positions only.

File Formats:
- 23andMe: Tab-delimited with columns: rsid, chromosome, position, genotype
  - mtDNA is chromosome "MT"
  - Header starts with "# This data file generated by 23andMe"
- AncestryDNA: Tab-delimited with columns: rsid, chromosome, position, allele1, allele2
  - mtDNA is chromosome "26"
  - Header contains "#AncestryDNA"
"""

from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional

from evehap.adapters.base import InputAdapter
from evehap.core.profile import AlleleObservation, AlleleProfile

if TYPE_CHECKING:
    from evehap.core.damage import DamageFilter


class MicroarrayAdapter(InputAdapter):
    """Adapter for consumer genotyping data (23andMe, AncestryDNA)."""

    # Chromosome names that indicate mtDNA
    MT_CHROMOSOMES = {"MT", "26", "M", "chrM", "chrMT"}

    def __init__(self, format_type: str = "auto", rsid_only: bool = True) -> None:
        """Initialize microarray adapter.

        Args:
            format_type: '23andme', 'ancestry', or 'auto' for detection
            rsid_only: If True, only use positions with curated rsIDs (more reliable).
                      If False, include all positions including internal probe IDs.
        """
        self.format_type = format_type
        self.rsid_only = rsid_only

    def can_handle(self, file_path: str) -> bool:
        """Check if file is a consumer genotype file."""
        if not file_path.endswith((".txt", ".csv")):
            return False

        file_path_obj = Path(file_path)
        if not file_path_obj.exists():
            return False

        return self._detect_format(file_path) is not None

    def _detect_format(self, file_path: str) -> Optional[str]:
        """Detect the format of a genotype file.

        Args:
            file_path: Path to the file

        Returns:
            Format name ('23andme', 'ancestry') or None if not recognized
        """
        file_path_obj = Path(file_path)
        if not file_path_obj.exists():
            return None

        try:
            with open(file_path, encoding="utf-8", errors="ignore") as f:
                # Read first 20 lines to detect format
                lines = []
                for i, line in enumerate(f):
                    if i >= 20:
                        break
                    lines.append(line)

                text = "".join(lines)

                # Check for 23andMe format
                if "23andMe" in text or "23andme" in text.lower():
                    return "23andme"

                # Check for AncestryDNA format
                if "AncestryDNA" in text or "ancestrydna" in text.lower():
                    return "ancestry"

                # Check column structure
                for line in lines:
                    if line.startswith("#"):
                        continue

                    parts = line.strip().split("\t")
                    if len(parts) >= 4:
                        # 23andMe style: rsid, chrom, pos, genotype
                        if parts[1].upper() in ("MT", "1", "2", "X", "Y"):
                            return "23andme"

                    if len(parts) >= 5:
                        # Ancestry style: rsid, chrom, pos, allele1, allele2
                        if parts[1] in ("26", "1", "2"):
                            return "ancestry"

                    break

        except Exception:
            pass

        return None

    @property
    def format_name(self) -> str:
        """Return format name."""
        return "Microarray"

    def extract_profile(
        self,
        file_path: str,
        damage_filter: Optional["DamageFilter"] = None,
        **kwargs: Any,
    ) -> AlleleProfile:
        """Extract profile from raw genotype file.

        Parses format-specific columns and extracts only mtDNA positions.

        Args:
            file_path: Path to genotype file
            damage_filter: Optional damage filter (not typically used)

        Returns:
            AlleleProfile extracted from the file

        Raises:
            FileNotFoundError: If file doesn't exist
            ValueError: If format cannot be detected
        """
        file_path_obj = Path(file_path)
        if not file_path_obj.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        # Determine format
        detected_format: Optional[str] = self.format_type
        if detected_format == "auto":
            detected_format = self._detect_format(file_path)
            if detected_format is None:
                raise ValueError(f"Cannot detect format of: {file_path}")

        # At this point detected_format is guaranteed to be a valid string
        assert detected_format is not None

        # Extract sample ID from filename
        sample_id = file_path_obj.stem

        # Create profile
        profile = AlleleProfile(
            sample_id=sample_id,
            source_format=self.format_name,
        )

        # Parse file based on format
        if detected_format == "23andme":
            self._parse_23andme(file_path, profile)
        elif detected_format == "ancestry":
            self._parse_ancestry(file_path, profile)
        else:
            raise ValueError(f"Unknown format: {detected_format}")

        return profile

    def _parse_23andme(self, file_path: str, profile: AlleleProfile) -> None:
        """Parse 23andMe format file.

        Format: rsid<tab>chromosome<tab>position<tab>genotype
        mtDNA is chromosome "MT"

        Note: 23andMe files using build 36/37 may have a +1 offset for mtDNA
        positions > 310 compared to rCRS (NC_012920) coordinates. We apply
        a -1 adjustment to convert to rCRS coordinates.

        Also, internal probe IDs (i-prefixed) may be less reliable than
        curated rsIDs, so we optionally filter to rsID-only positions.

        Args:
            file_path: Path to file
            profile: AlleleProfile to add observations to
        """
        # Detect if file uses build 36 coordinates (older 23andMe files)
        is_build36 = False
        with open(file_path, encoding="utf-8", errors="ignore") as f:
            for line in f:
                if "build 36" in line.lower() or "hg18" in line.lower():
                    is_build36 = True
                    break
                if not line.startswith("#"):
                    break

        with open(file_path, encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()

                # Skip comments and empty lines
                if not line or line.startswith("#"):
                    continue

                parts = line.split("\t")
                if len(parts) < 4:
                    continue

                rsid, chrom, pos_str, genotype = parts[:4]

                # Only process mtDNA
                if chrom.upper() not in self.MT_CHROMOSOMES:
                    continue

                # Filter to rsID-only if requested (internal IDs start with 'i')
                # rsIDs are more reliable as they are curated variants
                if self.rsid_only and not rsid.startswith("rs"):
                    continue

                try:
                    position = int(pos_str)
                except ValueError:
                    continue

                # Apply coordinate adjustment for build 36/37 to rCRS
                # Position > 310 in build 36/37 are offset by +1 from rCRS
                if is_build36 and position > 310:
                    position = position - 1

                # Parse genotype
                genotype = genotype.strip().upper()
                if not genotype or genotype == "--" or genotype == "0":
                    continue

                # For mtDNA, genotype is typically a single base
                # (haploid) but can be two bases if heteroplasmic
                alleles: Dict[str, float] = {}
                for base in genotype:
                    if base in "ACGT":
                        alleles[base] = alleles.get(base, 0) + 1

                if not alleles:
                    continue

                # Normalize frequencies
                total = sum(alleles.values())
                alleles = {k: v / total for k, v in alleles.items()}

                obs = AlleleObservation(
                    position=position,
                    ref_allele="N",  # Reference unknown from microarray
                    alleles=alleles,
                    depth=None,
                    quality=None,
                    source=self.format_name,
                )
                profile.add_observation(obs)

    def _parse_ancestry(self, file_path: str, profile: AlleleProfile) -> None:
        """Parse AncestryDNA format file.

        Format: rsid<tab>chromosome<tab>position<tab>allele1<tab>allele2
        mtDNA is chromosome "26"

        Args:
            file_path: Path to file
            profile: AlleleProfile to add observations to
        """
        with open(file_path, encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()

                # Skip comments and empty lines
                if not line or line.startswith("#"):
                    continue

                # Skip header
                if line.startswith("rsid"):
                    continue

                parts = line.split("\t")
                if len(parts) < 5:
                    continue

                rsid, chrom, pos_str, allele1, allele2 = parts[:5]

                # Only process mtDNA (chromosome 26 in Ancestry)
                if chrom != "26":
                    continue

                try:
                    position = int(pos_str)
                except ValueError:
                    continue

                # Parse alleles
                allele1 = allele1.strip().upper()
                allele2 = allele2.strip().upper()

                alleles: Dict[str, float] = {}
                for allele in [allele1, allele2]:
                    if allele in "ACGT":
                        alleles[allele] = alleles.get(allele, 0) + 1

                if not alleles:
                    continue

                # Normalize frequencies
                total = sum(alleles.values())
                alleles = {k: v / total for k, v in alleles.items()}

                obs = AlleleObservation(
                    position=position,
                    ref_allele="N",
                    alleles=alleles,
                    depth=None,
                    quality=None,
                    source=self.format_name,
                )
                profile.add_observation(obs)
