# -*- coding: utf-8 -*-
from __future__ import annotations
from fractions import Fraction
from typing import Any
import numpy as np
from .backends import BackendNotAvailableError, dispatch_structure_backend, normalize_backend, to_builtin

def _sga(structure: Any, symprec: float, angle_tolerance: float):
    from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
    return SpacegroupAnalyzer(structure, symprec=float(symprec), angle_tolerance=float(angle_tolerance))

def get_spg_inf(structure: Any, *, backend: str='auto', symprec: float=1e-3, angle_tolerance: float=5.0) -> dict[str, Any]:
    b=dispatch_structure_backend(structure, backend)
    if b=='pymatgen':
        s=_sga(structure,symprec,angle_tolerance)
        ret={'backend':'pymatgen','symprec':float(symprec),'angle_tolerance':float(angle_tolerance),
             'symbol':s.get_space_group_symbol(),'number':int(s.get_space_group_number()),
             'crystal_system':s.get_crystal_system(),'point_group':s.get_point_group_symbol()}
        for k,m in [('lattice_type','get_lattice_type'),('hall','get_hall')]:
            try: ret[k]=getattr(s,m)()
            except Exception: pass
        return ret
    raise RuntimeError(f'unsupported backend {b!r}')

def classify_symop(op: Any, tol: float=1e-5) -> str:
    R=np.asarray(op.rotation_matrix,float); t=np.asarray(op.translation_vector,float); zero=np.allclose(t,[0,0,0],atol=tol)
    if np.allclose(R,np.eye(3),atol=tol): return 'Identity' if zero else 'Pure Translation'
    if np.allclose(R,-np.eye(3),atol=tol): return 'Inversion' if zero else 'Inversion + Translation'
    det=float(np.linalg.det(R))
    if np.isclose(det,1,atol=tol): return 'Rotation' if zero else 'Rotation/Screw'
    if np.isclose(det,-1,atol=tol): return 'Mirror/Improper' if zero else 'Glide/Improper + Translation'
    return 'Unknown'

def _frac(x: float, max_denominator=12) -> str:
    if abs(x)<1e-10: return '0'
    f=Fraction(float(x)).limit_denominator(max_denominator)
    if abs(float(f)-float(x))<1e-6: return str(f.numerator) if f.denominator==1 else f'{f.numerator}/{f.denominator}'
    return f'{x:.6g}'

def format_symop_xyz(op: Any) -> str:
    R=np.asarray(op.rotation_matrix,float); t=np.asarray(op.translation_vector,float); labels=['x','y','z']; eq=[]
    for i,row in enumerate(R):
        terms=[]
        for j,c in enumerate(row):
            if abs(c)<1e-10: continue
            term=labels[j] if np.isclose(c,1) else (f'-{labels[j]}' if np.isclose(c,-1) else f'{_frac(c)}*{labels[j]}')
            terms.append(('+'+term) if terms and not term.startswith('-') else term)
        if abs(t[i])>1e-10:
            ts=_frac(float(t[i])); terms.append(('+'+ts) if terms and not ts.startswith('-') else ts)
        eq.append(''.join(terms) if terms else '0')
    return ', '.join(eq)

def _op_dict(op: Any, i: int) -> dict[str, Any]:
    d={'index':int(i),'kind':classify_symop(op),'rotation_matrix':to_builtin(op.rotation_matrix),
       'translation_vector':to_builtin(op.translation_vector),'xyz':format_symop_xyz(op)}
    for name in ['as_xyz_string','as_xyz_str']:
        try: d['pymatgen_xyz']=getattr(op,name)(); break
        except Exception: pass
    return d

def get_symmetry_operations_inf(structure: Any, *, backend: str='auto', symprec: float=1e-3, angle_tolerance: float=5.0, max_ops: int|None=None) -> dict[str, Any]:
    b=dispatch_structure_backend(structure, backend)
    if b=='pymatgen':
        ops=list(_sga(structure,symprec,angle_tolerance).get_symmetry_operations()); shown=ops if max_ops is None else ops[:int(max_ops)]
        return {'backend':'pymatgen','symprec':float(symprec),'angle_tolerance':float(angle_tolerance),
                'n_operations':len(ops),'n_returned_operations':len(shown),'operations':[_op_dict(op,i) for i,op in enumerate(shown)]}
    raise RuntimeError(f'unsupported backend {b!r}')

def symmetrize_structure(structure: Any, *, backend: str='auto', symprec: float=1e-3, angle_tolerance: float=5.0, mode: str='symmetrized') -> Any:
    b=dispatch_structure_backend(structure, backend)
    if b!='pymatgen': raise RuntimeError(f'unsupported backend {b!r}')
    s=_sga(structure,symprec,angle_tolerance); m=str(mode).lower().strip()
    if m=='symmetrized': return s.get_symmetrized_structure()
    if m=='refined': return s.get_refined_structure()
    if m=='conventional_standard': return s.get_conventional_standard_structure()
    if m=='primitive_standard': return s.get_primitive_standard_structure()
    raise ValueError(f'Unknown mode: {mode!r}')

def _unique(points, rmin):
    u=[]
    for p in points:
        p=np.mod(p,1.0); ok=True
        for q in u:
            d=np.abs(p-q); d=np.where(d>0.5,1.0-d,d)
            if np.linalg.norm(d)<rmin: ok=False; break
        if ok: u.append(p)
    return u

def expand_coordinates_spg(ispg: int, xyz: str|list[float]|tuple[float,float,float], *, backend: str='pymatgen', rmin: float=1e-5, max_ops: int|None=None) -> dict[str, Any]:
    b=normalize_backend(backend); b='pymatgen' if b=='auto' else b
    if b!='pymatgen': raise BackendNotAvailableError(f'backend not implemented: {backend!r}')
    from pymatgen.symmetry.groups import SpaceGroup
    p0=np.array([float(x.strip()) for x in xyz.split(',')] if isinstance(xyz,str) else list(xyz), float)
    if p0.shape[0]!=3: raise ValueError('xyz must have 3 components')
    spg=SpaceGroup.from_int_number(int(ispg)); ops=list(spg.symmetry_ops); used=ops if max_ops is None else ops[:int(max_ops)]
    pts=[]; details=[]
    for i,op in enumerate(used):
        p=np.mod(op.operate(p0),1.0); pts.append(p); details.append({'index':i,'xyz':format_symop_xyz(op),'kind':classify_symop(op),'frac_coords':[float(x) for x in p]})
    uni=_unique(pts,float(rmin))
    return {'backend':'pymatgen','spacegroup_number':int(ispg),'spacegroup_symbol':spg.symbol,'input_frac_coords':[float(x) for x in p0],
            'rmin':float(rmin),'n_operations':len(ops),'n_used_operations':len(used),'n_expanded_points':len(pts),'n_unique_points':len(uni),
            'unique_frac_coords':[[float(x) for x in p] for p in uni],'operations':details}
